本教程介绍如何在 Apache Solr 中实现现代学习排序(LTR,也称为机器学习排序)系统。它面向那些没有 Solr 经验,但熟悉机器学习和信息检索概念的人。几个月前,我也是这些人中的一员,我发现在线找到的 Solr 材料极具挑战性,难以入门。本文是我尝试编写我希望在入门时拥有的教程。
目录
设置 Solr
在 Linux(我的情况是 Fedora)上启动一个原始的 Solr 实例实际上非常简单。在 Fedora 上,首先下载 Solr 源代码 tarball(即,包含“src”的文件)并将其解压缩到合理的位置。接下来,cd 进入 Solr 目录
cd /path/to/solr-<version>/solr
构建 Solr 需要 Apache Ant 和 Apache Ivy,因此请安装它们
sudo dnf install ant ivy
现在构建 Solr
ant server
您可以运行以下命令确认 Solr 是否正常工作
bin/solr start
并确保您在 http://localhost:8983/solr/ 看到 Solr 管理界面。您可以使用以下命令停止 Solr(但现在不要停止)
bin/solr stop
Solr 基础知识
Solr 是一个搜索平台,因此您只需要知道如何执行两件事即可正常工作:索引数据和定义排序模型。Solr 具有类似 REST 的 API,这意味着更改将通过 curl 命令进行。要开始使用,请创建一个名为 test 的 core
bin/solr create -c test
这个看似简单的命令实际上在幕后做了很多事情。具体来说,它定义了一个 schema,该 schema 告诉 Solr 如何处理文档(考虑 分词、词干提取等)和搜索(例如,使用 tf-idf 向量空间模型),并设置了一个 配置文件,用于指定 Solr 将使用的库和处理程序。可以使用以下命令删除 core
bin/solr delete -c test
好的,让我们添加一些文档。首先下载此 tweets.xml XML 文件,该文件在 Solr in Action GitHub 上提供。查看 XML 文件内部。请注意,它如何使用 <add> 标签来告诉 Solr 添加多个文档(用 <doc> 标签表示)到索引。要索引推文,请运行
bin/post -c test /path/to/tweets.xml
如果您现在转到 http://localhost:8983/solr/(您可能需要刷新)并单击左侧的“Core Selector”下拉列表,则可以选择 test core。如果您然后单击“Query”选项卡,则查询界面将出现。如果您单击底部的蓝色“Execute Query”按钮,则将显示一个 JSON 文档,其中包含有关刚刚索引的推文的信息。恭喜您,您刚刚成功运行了您的第一个查询!具体来说,您使用了 /select RequestHandler 来执行查询 *:*。*:* 是一种特殊语法,用于告诉 Solr 返回所有内容。Solr 查询语法在我看来不是很直观,所以您只需要习惯它。
定义特征
现在您已经启动并运行了一个基本的 Solr 实例,请为您的 LTR 系统定义特征。与所有机器学习问题一样,有效的特征工程对于成功至关重要。现代 LTR 模型中的标准特征包括使用多种相似性度量(例如,tf-idf 向量或 BM25 的 余弦相似度)来比较多个文本字段(例如,正文、标题),以及其他文本特征(例如,长度)和文档特征(例如,年龄、PageRank)。一个好的起点是微软研究院的学术数据集特征列表。有关其他常用特征的列表,请参见马萨诸塞大学阿默斯特分校研究员 Jiepu Jiang 讲义的 幻灯片 32。
首先,修改 /path/to/solr-<version>/solr/server/solr/test/conf/managed-schema,使其包含您的模型所需的文本字段。首先,更改 text 字段,使其类型为 text_general 类型(该类型已在 managed-schema 中定义)。text_general 类型将允许您计算 BM25 相似度。由于 text 字段已经存在(它是在您索引推文时自动创建的),因此您需要使用 replace-field 命令,如下所示
curl -X POST -H 'Content-type:application/json' --data-binary '{
"replace-field" : {
"name":"text",
"type":"text_general",
"indexed":"true",
"stored":"true",
"multiValued":"true"}
}' http://localhost:8983/solr/test/schema
我鼓励您在每次更改后查看 managed-schema 内部,以便了解正在发生的事情。接下来,指定 text_tfidf 类型,这将允许您计算 tf-idf 余弦相似度
curl -X POST -H 'Content-type:application/json' --data-binary '{
"add-field-type" : {
"name":"text_tfidf",
"class":"solr.TextField",
"positionIncrementGap":"100",
"indexAnalyzer":{
"tokenizer":{
"class":"solr.StandardTokenizerFactory"},
"filter":{
"class":"solr.StopFilterFactory",
"ignoreCase":"true",
"words":"stopwords.txt"},
"filter":{
"class":"solr.LowerCaseFilterFactory"}},
"queryAnalyzer":{
"tokenizer":{
"class":"solr.StandardTokenizerFactory"},
"filter":{
"class":"solr.StopFilterFactory",
"ignoreCase":"true",
"words":"stopwords.txt"},
"filter":{
"class":"solr.SynonymGraphFilterFactory",
"ignoreCase":"true",
"synonyms":"synonyms.txt"},
"filter":{
"class":"solr.LowerCaseFilterFactory"}},
"similarity":{
"class":"solr.ClassicSimilarityFactory"}}
}' http://localhost:8983/solr/test/schema
现在添加一个 text_tfidf 字段,该字段将是您刚刚定义的 text_tfidf 类型
curl -X POST -H 'Content-type:application/json' --data-binary '{
"add-field" : {
"name":"text_tfidf",
"type":"text_tfidf",
"indexed":"true",
"stored":"false",
"multiValued":"true"}
}' http://localhost:8983/solr/test/schema
由于 text 字段和 text_tfidf 字段的内容相同(它们只是以不同的方式处理),因此请告诉 Solr 将内容从 text 复制到 text_tfidf
curl -X POST -H 'Content-type:application/json' --data-binary '{
"add-copy-field" : {
"source":"text",
"dest":"text_tfidf"}
}' http://localhost:8983/solr/test/schema
现在您可以重新索引您的数据了
bin/post -c test /home/malcorn/solr-in-action/example-docs/ch6/tweets.xml
学习排序
现在您的文档已正确索引,请构建 LTR 模型。如果您是 LTR 新手,我建议您查看 Tie-Yan Liu 的(长篇)论文和 教科书。如果您熟悉机器学习,那么这些想法应该不难掌握。我还建议您查看 Solr 关于 LTR 的文档,我将在本节中链接到该文档。在 Solr 中启用 LTR 首先需要对 /path/to/solr-<version>/solr/server/solr/test/solrconfig.xml 进行一些更改。将以下文本复制并粘贴到 <config> 和 </config> 标签之间的任何位置(分别位于文件的顶部和底部)。
<lib dir="${solr.install.dir:../../../..}/contrib/ltr/lib/" regex=".*\.jar" />
<lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-ltr-\d.*\.jar" />
<queryParser name="ltr" class="org.apache.solr.ltr.search.LTRQParserPlugin"/>
<cache name="QUERY_DOC_FV"
class="solr.search.LRUCache"
size="4096"
initialSize="2048"
autowarmCount="4096"
regenerator="solr.search.NoOpRegenerator" />
<transformer name="features" class="org.apache.solr.ltr.response.transform.LTRFeatureLoggerTransformerFactory">
<str name="fvCacheName">QUERY_DOC_FV</str>
</transformer>
现在您已准备好在启用 LTR 的情况下运行 Solr。首先,停止 Solr
bin/solr stop
然后使用启用的 LTR 插件重新启动它
bin/solr start -Dsolr.ltr.enabled=true
接下来,将模型特征和模型规范推送到 Solr。在 Solr 中,LTR 特征是使用 JSON 格式的文件定义的。对于此模型,将以下特征保存在 my_efi_features.json 中
[
{
"store" : "my_efi_feature_store",
"name" : "tfidf_sim_a",
"class" : "org.apache.solr.ltr.feature.SolrFeature",
"params" : { "q" : "{!dismax qf=text_tfidf}${text_a}" }
},
{
"store" : "my_efi_feature_store",
"name" : "tfidf_sim_b",
"class" : "org.apache.solr.ltr.feature.SolrFeature",
"params" : { "q" : "{!dismax qf=text_tfidf}${text_b}" }
},
{
"store" : "my_efi_feature_store",
"name" : "bm25_sim_a",
"class" : "org.apache.solr.ltr.feature.SolrFeature",
"params" : { "q" : "{!dismax qf=text}${text_a}" }
},
{
"store" : "my_efi_feature_store",
"name" : "bm25_sim_b",
"class" : "org.apache.solr.ltr.feature.SolrFeature",
"params" : { "q" : "{!dismax qf=text}${text_b}" }
},
{
"store" : "my_efi_feature_store",
"name" : "max_sim",
"class" : "org.apache.solr.ltr.feature.SolrFeature",
"params" : { "q" : "{!dismax qf='text text_tfidf'}${text}" }
}
]
命令 store 告诉 Solr 特征的存储位置;name 是特征的名称;class 指定哪个 Java 类将处理该特征;而 params 提供其 Java 类所需的有关特征的附加信息。对于 SolrFeature,您需要提供查询。{!dismax qf=text_tfidf}${text_a} 告诉 Solr 使用 DisMaxQParser 在 text_tfidf 字段中搜索 text_a 的内容。使用 DisMax 解析器而不是看似更明显的 FieldQParser(例如,{!field f=text_tfidf}${text_a})的原因是,FieldQParser 会自动将多词查询转换为“短语”(即,它将“the cat in the hat”之类的东西有效地转换为“thecatinthehat”,而不是“the”、“cat”、“in”、“the”、“hat”)。这种 FieldQParser 行为(在我看来,这似乎是一个相当奇怪的默认行为)最终让我非常头疼,但我最终使用 DisMaxQParser 找到了解决方案。
{!dismax qf='text text_tfidf'}${text} 告诉 Solr 在 text 和 text_tfidf 字段中搜索 text 的内容,然后取这两个分数的最大值。虽然此特征在此上下文中实际上没有任何意义,因为来自两个字段的相似性已用作特征,但它演示了如何实现此类特征。例如,假设语料库中的文档最多链接到五个其他文本数据源。在搜索期间合并该信息可能是有意义的,而取多个相似性分数的最大值是执行此操作的一种方法。
要将特征推送到 Solr,请运行以下命令
curl -XPUT 'http://localhost:8983/solr/test/schema/feature-store' --data-binary "@/path/to/my_efi_features.json" -H 'Content-type:application/json'
如果要上传新特征,则必须首先使用以下命令删除旧特征
curl -XDELETE 'http://localhost:8983/solr/test/schema/feature-store/my_efi_feature_store'
接下来,将以下模型规范保存在 my_efi_model.json 中
{
"store" : "my_efi_feature_store",
"name" : "my_efi_model",
"class" : "org.apache.solr.ltr.model.LinearModel",
"features" : [
{ "name" : "tfidf_sim_a" },
{ "name" : "tfidf_sim_b" },
{ "name" : "bm25_sim_a" },
{ "name" : "bm25_sim_b" },
{ "name" : "max_sim" }
],
"params" : {
"weights" : {
"tfidf_sim_a" : 1.0,
"tfidf_sim_b" : 1.0,
"bm25_sim_a" : 1.0,
"bm25_sim_b" : 1.0,
"max_sim" : 0.5
}
}
}
在这种情况下,store 指定 模型正在使用的特征的存储位置;name 是模型的名称;class 指定哪个 Java 类将实现该模型;features 是模型特征的列表;而 params 提供模型 Java 类所需的其他信息。首先使用 LinearModel,它只是对特征值进行加权求和以生成分数。显然,提供的权重是任意的。要找到更好的权重,您需要从 Solr 中提取训练数据。我将在 RankNet 部分中更深入地讨论这个主题。
您可以使用以下命令将模型推送到 Solr
curl -XPUT 'http://localhost:8983/solr/test/schema/model-store' --data-binary "@/path/to/my_efi_model.json" -H 'Content-type:application/json'
现在您可以运行您的第一个 LTR 查询了
您应该看到类似以下内容
{
"responseHeader":{
"status":0,
"QTime":101,
"params":{
"q":"historic north",
"fl":"id,score,[features]",
"rq":"{!ltr model=my_efi_model efi.text_a=historic efi.text_b=north efi.text='historic north'}"}},
"response":{"numFound":1,"start":0,"maxScore":3.0671878,"docs":[
{
"id":"1",
"score":3.0671878,
"[features]":"tfidf_sim_a=0.53751516,tfidf_sim_b=0.0,bm25_sim_a=0.84322417,bm25_sim_b=0.84322417,max_sim=1.6864483"}]
}
}
参考请求,q=historic north 是用于获取初始结果的查询(在这种情况下使用 BM25),然后使用 LTR 模型重新排序。rq 是提供所有 LTR 参数的位置,而 efi 代表“外部特征信息”,它允许您在查询时指定特征。在这种情况下,您正在使用术语 historic 填充 text_a 参数,使用术语 north 填充 text_b 参数,并使用多词查询 'historic north' 填充 text 参数(请注意,这不被视为“短语”)。fl=id,score,[features] 告诉 Solr 在结果中包含 id、score 和模型特征。您可以通过在 Solr 管理 UI 的“Query”界面中执行关联搜索来验证特征值是否正确。例如,在 q 文本框中键入 text_tfidf:historic,在 fl 文本框中键入 score,然后单击“Execute Query”按钮应返回 0.53751516 的值。
RankNet
对于 LTR 系统,线性模型通常使用所谓的“逐点”方法进行训练,其中文档被单独考虑(即,模型会问,“此文档与查询相关还是不相关?”);但是,逐点方法通常不太适合 LTR 问题。RankNet 是一种神经网络,它使用“成对”方法,其中具有已知相对偏好的文档成对考虑(即,模型会问,“对于查询,文档 A 比文档 B 更相关吗?”)。Solr 不开箱即用地支持 RankNet,但我已经在 Solr 和 Keras 中实现了 RankNet。值得注意的是,LambdaMART 可能更适合您的搜索应用程序。但是,可以使用我的 Keras 实现在 GPU 上快速训练 RankNet,这使其成为仅有一个文档与任何给定查询相关的搜索问题的良好解决方案。有关 RankNet、LambdaRank 和 LambdaMART 的出色(技术性)概述,请参见 Chris Burges 在微软研究院撰写的论文。
要在 Solr 中启用 RankNet,您必须将 RankNet.java 添加到 /path/to/solr-<version>/solr/contrib/ltr/src/java/org/apache/solr/ltr/model,然后重新构建 Solr(提醒:在 /path/to/solr-<version>/solr 中构建)
ant server
现在,如果您检查 /path/to/solr-<version>/solr/dist/solr-ltr-{version}-SNAPSHOT.jar,您应该在 /org/apache/solr/ltr/model/ 下看到 RankNet.class。
不幸的是,Solr 中 建议的特征提取方法速度非常慢(其他 Solr 用户似乎也同意它可以更快)。即使并行发出请求,我也花了将近三天的时间才提取出约 200,000 个查询的特征。我认为一个更好的方法可能是索引查询,然后计算“文档”(由真实文档和查询组成)之间的相似性,但这确实应该内置到 Solr 中。无论如何,这里有一些示例 Python 代码,用于使用查询从 Solr 中提取特征
import numpy as np
import requests
import simplejson
# Number of documents to be re-ranked.
RERANK = 50
with open("RERANK.int", "w") as f:
f.write(str(RERANK))
# Build query URL.
q_id = row["id"]
q_field_a = row["field_a"].strip().lower()
q_field_b = row["field_b"].strip().lower()
q_field_c = row["field_c"].strip().lower()
q_field_d = row["field_d"].strip().lower()
all_text = " ".join([q_field_a, q_field_b, q_field_c, q_field_d])
url = "http://localhost:8983/solr/test/query"
# We only re-rank one document when extracting features because we want to be
# able to compare the LTR model to the BM25 ranking. Setting reRankDocs=1
# ensures the original ranking is maintained.
url += "?q={0}&rq={{!ltr model=my_efi_model reRankDocs=1 ".format(all_text)
url += "efi.field_a='{0}' efi.field_b='{1}' efi.field_c='{2}' efi.field_d='{3}' ".format(field_a, field_b, field_c, field_d)
url += "efi.all_text='{0}'}}&fl=id,score,[features]&rows={1}".format(all_text, RERANK)
# Get response and check for errors.
response = requests.request("GET", url)
try:
json = simplejson.loads(response.text)
except simplejson.JSONDecodeError:
print(q_id)
return
if "error" in json:
print(q_id)
return
# Extract the features.
results_features = []
results_targets = []
results_ranks = []
add_data = False
for (rank, document) in enumerate(json["response"]["docs"]):
features = document["[features]"].split(",")
feature_array = []
for feature in features:
feature_array.append(feature.split("=")[1])
feature_array = np.array(feature_array, dtype = "float32")
results_features.append(feature_array)
doc_id = document["id"]
# Check if document is relevant to query.
if q_id in relevant.get(doc_id, {}):
results_ranks.append(rank + 1)
results_targets.append(1)
add_data = True
else:
results_targets.append(0)
if add_data:
np.save("{0}_X.npy".format(q_id), np.array(results_features))
np.save("{0}_y.npy".format(q_id), np.array(results_targets))
np.save("{0}_rank.npy".format(q_id), np.array(results_ranks))
现在您可以训练一些模型了。首先,提取数据并评估整个数据集上的 BM25 排名。
import glob
import numpy as np
rank_files = glob.glob("*_rank.npy")
suffix_len = len("_rank.npy")
RERANK = int(open("RERANK.int").read())
ranks = []
casenumbers = []
Xs = []
ys = []
for rank_file in rank_files:
X = np.load(rank_file[:-suffix_len] + "_X.npy")
casenumbers.append(rank_file[:suffix_len])
if X.shape[0] != RERANK:
print(rank_file[:-suffix_len])
continue
rank = np.load(rank_file)[0]
ranks.append(rank)
y = np.load(rank_file[:-suffix_len] + "_y.npy")
Xs.append(X)
ys.append(y)
ranks = np.array(ranks)
total_docs = len(ranks)
print("Total Documents: {0}".format(total_docs))
print("Top 1: {0}".format((ranks == 1).sum() / total_docs))
print("Top 3: {0}".format((ranks <= 3).sum() / total_docs))
print("Top 5: {0}".format((ranks <= 5).sum() / total_docs))
print("Top 10: {0}".format((ranks <= 10).sum() / total_docs))
接下来,构建并评估(逐点)线性支持向量机。
from scipy.stats import rankdata
from sklearn.svm import LinearSVC
X = np.concatenate(Xs, 0)
y = np.concatenate(ys)
train_per = 0.8
train_cutoff = int(train_per * len(ranks)) * RERANK
train_X = X[:train_cutoff]
train_y = y[:train_cutoff]
test_X = X[train_cutoff:]
test_y = y[train_cutoff:]
model = LinearSVC()
model.fit(train_X, train_y)
preds = model._predict_proba_lr(test_X)
n_test = int(len(test_y) / RERANK)
new_ranks = []
for i in range(n_test):
start = i * RERANK
end = start + RERANK
scores = preds[start:end, 1]
score_ranks = rankdata(-scores)
old_rank = np.argmax(test_y[start:end])
new_rank = score_ranks[old_rank]
new_ranks.append(new_rank)
new_ranks = np.array(new_ranks)
print("Total Documents: {0}".format(n_test))
print("Top 1: {0}".format((new_ranks == 1).sum() / n_test))
print("Top 3: {0}".format((new_ranks <= 3).sum() / n_test))
print("Top 5: {0}".format((new_ranks <= 5).sum() / n_test))
print("Top 10: {0}".format((new_ranks <= 10).sum() / n_test))
现在您可以尝试 RankNet 了。首先,组装训练数据,以便每一行都包含与不相关文档向量连接的相关文档向量(对于给定的查询)。由于在特征提取阶段返回了 50 行,因此每个查询在数据集中将有 49 个文档对。
Xs = []
for rank_file in rank_files:
X = np.load(rank_file[:-suffix_len] + "_X.npy")
if X.shape[0] != RERANK:
print(rank_file[:-suffix_len])
continue
rank = np.load(rank_file)[0]
pos_example = X[rank - 1]
for (i, neg_example) in enumerate(X):
if i == rank - 1:
continue
Xs.append(np.concatenate((pos_example, neg_example)))
X = np.stack(Xs)
dim = int(X.shape[1] / 2)
train_per = 0.8
train_cutoff = int(train_per * len(ranks)) * (RERANK - 1)
train_X = X[:train_cutoff]
test_X = X[train_cutoff:]
在 Keras 中构建模型
from keras import backend
from keras.callbacks import ModelCheckpoint
from keras.layers import Activation, Add, Dense, Input, Lambda
from keras.models import Model
y = np.ones((train_X.shape[0], 1))
INPUT_DIM = dim
h_1_dim = 64
h_2_dim = h_1_dim // 2
h_3_dim = h_2_dim // 2
# Model.
h_1 = Dense(h_1_dim, activation = "relu")
h_2 = Dense(h_2_dim, activation = "relu")
h_3 = Dense(h_3_dim, activation = "relu")
s = Dense(1)
# Relevant document score.
rel_doc = Input(shape = (INPUT_DIM, ), dtype = "float32")
h_1_rel = h_1(rel_doc)
h_2_rel = h_2(h_1_rel)
h_3_rel = h_3(h_2_rel)
rel_score = s(h_3_rel)
# Irrelevant document score.
irr_doc = Input(shape = (INPUT_DIM, ), dtype = "float32")
h_1_irr = h_1(irr_doc)
h_2_irr = h_2(h_1_irr)
h_3_irr = h_3(h_2_irr)
irr_score = s(h_3_irr)
# Subtract scores.
negated_irr_score = Lambda(lambda x: -1 * x, output_shape = (1, ))(irr_score)
diff = Add()([rel_score, negated_irr_score])
# Pass difference through sigmoid function.
prob = Activation("sigmoid")(diff)
# Build model.
model = Model(inputs = [rel_doc, irr_doc], outputs = prob)
model.compile(optimizer = "adagrad", loss = "binary_crossentropy")
现在训练和测试模型
NUM_EPOCHS = 30
BATCH_SIZE = 32
checkpointer = ModelCheckpoint(filepath = "valid_params.h5", verbose = 1, save_best_only = True)
history = model.fit([train_X[:, :dim], train_X[:, dim:]], y,
epochs = NUM_EPOCHS, batch_size = BATCH_SIZE, validation_split = 0.05,
callbacks = [checkpointer], verbose = 2)
model.load_weights("valid_params.h5")
get_score = backend.function([rel_doc], [rel_score])
n_test = int(test_X.shape[0] / (RERANK - 1))
new_ranks = []
for i in range(n_test):
start = i * (RERANK - 1)
end = start + (RERANK - 1)
pos_score = get_score([test_X[start, :dim].reshape(1, dim)])[0]
neg_scores = get_score([test_X[start:end, dim:]])[0]
scores = np.concatenate((pos_score, neg_scores))
score_ranks = rankdata(-scores)
new_rank = score_ranks[0]
new_ranks.append(new_rank)
new_ranks = np.array(new_ranks)
print("Total Documents: {0}".format(n_test))
print("Top 1: {0}".format((new_ranks == 1).sum() / n_test))
print("Top 3: {0}".format((new_ranks <= 3).sum() / n_test))
print("Top 5: {0}".format((new_ranks <= 5).sum() / n_test))
print("Top 10: {0}".format((new_ranks <= 10).sum() / n_test))
# Compare to BM25.
old_ranks = ranks[-n_test:]
print("Total Documents: {0}".format(n_test))
print("Top 1: {0}".format((old_ranks == 1).sum() / n_test))
print("Top 3: {0}".format((old_ranks <= 3).sum() / n_test))
print("Top 5: {0}".format((old_ranks <= 5).sum() / n_test))
print("Top 10: {0}".format((old_ranks <= 10).sum() / n_test))
如果模型的结果令人满意,请将参数保存到 JSON 文件中以推送到 Solr
import json
weights = model.get_weights()
solr_model = json.load(open("my_efi_model.json"))
solr_model["class"] = "org.apache.solr.ltr.model.RankNet"
solr_model["params"]["weights"] = []
for i in range(len(weights) // 2):
matrix = weights[2 * i].T
bias = weights[2 * i + 1]
bias = bias.reshape(bias.shape[0], 1)
out_matrix = np.hstack((matrix, bias))
np.savetxt("layer_{0}.csv".format(i), out_matrix, delimiter = ",")
matrix_str = open("layer_{0}.csv".format(i)).read().strip()
solr_model["params"]["weights"].append(matrix_str)
solr_model["params"]["nonlinearity"] = "relu"
with open("my_efi_model.json", "w") as out:
json.dump(solr_model, out, indent = 4)
并像以前一样推送它(在删除之后)
curl -XDELETE 'http://localhost:8983/solr/test/schema/model-store/my_efi_model'
curl -XPUT 'http://localhost:8983/solr/test/schema/model-store' --data-binary "@/path/to/my_efi_model.json" -H 'Content-type:application/json'
这就是您在 Apache Solr 中的现代学习排序设置。
评论已关闭。