Skip to content

Commit 673b0ea

Browse files
authored
Support Highlighter for Search (#524)
Signed-off-by: yhmo <yihua.mo@zilliz.com>
1 parent 426cbf5 commit 673b0ea

14 files changed

Lines changed: 1204 additions & 6 deletions

File tree

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Examples for MilvusClientV2:
3737
- `./cmake_build/examples/v2/sdk_filter_template_v2`: example to show the usage of filter template.
3838
- `./cmake_build/examples/v2/sdk_full_text_match_v2`: example to show the usage of BM25 function.
3939
- `./cmake_build/examples/v2/sdk_general_v2`: a general example to show the basic usage.
40+
- `./cmake_build/examples/v2/sdk_highlighter_v2`: example to show the usage of lexical highlighter with BM25 text search.
4041
- `./cmake_build/examples/v2/sdk_geometry_field_v2`: a general example to show the usage of Geometry field.
4142
- `./cmake_build/examples/v2/sdk_group_by_v2`: a general example to show the usage of grouping search.
4243
- `./cmake_build/examples/v2/sdk_hybrid_search_v2`: example to show the usage of hybrid search interface.

examples/src/v2/highlighter.cpp

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// Licensed to the LF AI & Data foundation under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
#include "milvus/types/Highlighter.h"
18+
19+
#include <iostream>
20+
#include <memory>
21+
#include <string>
22+
#include <thread>
23+
24+
#include "ExampleUtils.h"
25+
#include "milvus/MilvusClientV2.h"
26+
27+
/**
28+
* Demonstrates lexical highlighter usage for BM25 search results.
29+
*
30+
* Prerequisites:
31+
* - A running Milvus instance on localhost:19530
32+
* - Server-side text search and highlighter support enabled
33+
*/
34+
namespace {
35+
const char* const collection_name = "java_sdk_example_highlighter_v2";
36+
const char* const field_id = "id";
37+
const char* const field_title = "title";
38+
const char* const field_vector = "vector";
39+
const char* const field_text = "text";
40+
41+
std::string
42+
JoinQueryTexts(const std::vector<std::string>& query_texts) {
43+
std::string joined;
44+
for (size_t i = 0; i < query_texts.size(); ++i) {
45+
if (i != 0) {
46+
joined += " ";
47+
}
48+
joined += query_texts.at(i);
49+
}
50+
return joined;
51+
}
52+
53+
std::string
54+
FormatQueryTextsForDisplay(const std::vector<std::string>& query_texts) {
55+
std::string formatted = "[";
56+
for (size_t i = 0; i < query_texts.size(); ++i) {
57+
if (i != 0) {
58+
formatted += ", ";
59+
}
60+
formatted += query_texts.at(i);
61+
}
62+
formatted += "]";
63+
return formatted;
64+
}
65+
66+
void
67+
createCollection(milvus::MilvusClientV2Ptr& client) {
68+
milvus::CollectionSchemaPtr collection_schema = std::make_shared<milvus::CollectionSchema>();
69+
collection_schema->AddField({field_id, milvus::DataType::INT64, "", true, false});
70+
collection_schema->AddField(milvus::FieldSchema(field_title, milvus::DataType::VARCHAR)
71+
.WithMaxLength(512)
72+
.EnableAnalyzer(true)
73+
.EnableMatch(true));
74+
collection_schema->AddField(milvus::FieldSchema(field_text, milvus::DataType::VARCHAR)
75+
.WithMaxLength(65535)
76+
.EnableAnalyzer(true)
77+
.EnableMatch(true));
78+
collection_schema->AddField(milvus::FieldSchema(field_vector, milvus::DataType::SPARSE_FLOAT_VECTOR));
79+
80+
milvus::FunctionPtr function = std::make_shared<milvus::Function>("function_bm25", milvus::FunctionType::BM25);
81+
function->AddInputFieldName(field_title);
82+
function->AddOutputFieldName(field_vector);
83+
collection_schema->AddFunction(function);
84+
85+
auto status = client->DropCollection(milvus::DropCollectionRequest().WithCollectionName(collection_name));
86+
milvus::IndexDesc index_vector(field_vector, "", milvus::IndexType::SPARSE_INVERTED_INDEX,
87+
milvus::MetricType::BM25);
88+
status = client->CreateCollection(milvus::CreateCollectionRequest()
89+
.WithCollectionName(collection_name)
90+
.WithCollectionSchema(collection_schema)
91+
.AddIndex(std::move(index_vector))
92+
.WithConsistencyLevel(milvus::ConsistencyLevel::BOUNDED));
93+
util::CheckStatus(std::string("create collection: ") + collection_name, status);
94+
95+
status = client->LoadCollection(milvus::LoadCollectionRequest().WithCollectionName(collection_name));
96+
util::CheckStatus(std::string("load collection: ") + collection_name, status);
97+
std::cout << "Collection created: " << collection_name << std::endl;
98+
}
99+
100+
void
101+
insertData(milvus::MilvusClientV2Ptr& client) {
102+
milvus::EntityRows rows;
103+
104+
milvus::EntityRow row0;
105+
row0[field_id] = 0;
106+
row0[field_title] = "Milvus for scale";
107+
row0[field_text] =
108+
"Milvus is an open-source vector database built for scale. This paragraph is intentionally long so the keyword "
109+
"search appears much later in the same text fragment. Search is a core capability for information retrieval "
110+
"systems.";
111+
rows.emplace_back(std::move(row0));
112+
113+
milvus::EntityRow row1;
114+
row1[field_id] = 1;
115+
row1[field_title] = "Full text search";
116+
row1[field_text] =
117+
"Milvus supports full text search with analyzers and BM25. This sentence adds enough spacing and extra wording "
118+
"to separate the two highlighted terms into different regions for the lexical highlighter example.";
119+
rows.emplace_back(std::move(row1));
120+
121+
milvus::EntityRow row2;
122+
row2[field_id] = 2;
123+
row2[field_title] = "RAG systems";
124+
row2[field_text] = "Vector databases help retrieval augmented generation systems.";
125+
rows.emplace_back(std::move(row2));
126+
127+
milvus::EntityRow row3;
128+
row3[field_id] = 3;
129+
row3[field_title] = "Milvus users";
130+
row3[field_text] =
131+
"This example demonstrates highlighted snippets for modern applications. The word search is placed here with a "
132+
"lot of filler text before Milvus appears again near the end of the document to encourage multiple fragments "
133+
"in highlighter output for Milvus users.";
134+
rows.emplace_back(std::move(row3));
135+
136+
milvus::InsertResponse resp_insert;
137+
auto status = client->Insert(
138+
milvus::InsertRequest().WithCollectionName(collection_name).WithRowsData(std::move(rows)), resp_insert);
139+
util::CheckStatus("insert", status);
140+
std::cout << "Inserted " << resp_insert.Results().InsertCount() << " rows" << std::endl;
141+
142+
status = client->Flush(milvus::FlushRequest().AddCollectionName(collection_name));
143+
util::CheckStatus("flush collection", status);
144+
}
145+
146+
void
147+
PrintHighlightResult(const milvus::HighlightResults& highlight_results, const std::string& field_name) {
148+
auto iter = highlight_results.find(field_name);
149+
if (iter == highlight_results.end()) {
150+
return;
151+
}
152+
153+
const auto& highlight_result = iter->second;
154+
std::cout << " highlighted field: " << highlight_result.field_name << std::endl;
155+
std::cout << " fragments: " << nlohmann::json(highlight_result.fragments).dump() << std::endl;
156+
std::cout << " scores: " << nlohmann::json(highlight_result.scores).dump() << std::endl;
157+
}
158+
159+
void
160+
searchWithHighlighter(milvus::MilvusClientV2Ptr& client, const std::vector<std::string>& query_texts) {
161+
auto highlighter = std::make_shared<milvus::LexicalHighlighter>();
162+
highlighter->AddPreTag("<em>").AddPostTag("</em>").WithFragmentSize(40).WithNumOfFragments(10);
163+
for (const auto& query_text : query_texts) {
164+
highlighter->AddHighlightQuery("TextMatch", field_text, query_text);
165+
}
166+
167+
auto request =
168+
milvus::SearchRequest()
169+
.WithCollectionName(collection_name)
170+
.WithAnnsField(field_vector)
171+
.WithFilter(std::string("TEXT_MATCH(") + field_text + ", \"" + JoinQueryTexts(query_texts) + "\")")
172+
.WithLimit(3)
173+
.WithMetricType(milvus::MetricType::BM25)
174+
.AddOutputField(field_title)
175+
.AddOutputField(field_text)
176+
.WithHighlighter(highlighter);
177+
for (const auto& query_text : query_texts) {
178+
request.AddEmbeddedText(query_text);
179+
}
180+
181+
milvus::SearchResponse response;
182+
auto status = client->Search(request, response);
183+
util::CheckStatus("search", status);
184+
185+
std::cout << "\nSearch with lexical highlighter: " << FormatQueryTextsForDisplay(query_texts) << std::endl;
186+
for (const auto& result : response.Results().Results()) {
187+
std::cout << "\n-----------------------------------------------------------------------------" << std::endl;
188+
for (int i = 0; i < static_cast<int>(result.GetRowCount()); ++i) {
189+
milvus::EntityRow row;
190+
status = result.OutputRow(i, row);
191+
util::CheckStatus("get output row", status);
192+
std::cout << row << std::endl;
193+
194+
milvus::HighlightResults highlight_results;
195+
status = result.OutputHighlightResult(i, highlight_results);
196+
util::CheckStatus("get highlight result", status);
197+
PrintHighlightResult(highlight_results, field_text);
198+
PrintHighlightResult(highlight_results, field_title);
199+
std::cout << " title: " << row[field_title] << std::endl;
200+
std::cout << " text: " << row[field_text] << std::endl;
201+
}
202+
}
203+
std::cout << "=============================================================" << std::endl;
204+
}
205+
206+
void
207+
runExample(milvus::MilvusClientV2Ptr& client) {
208+
createCollection(client);
209+
insertData(client);
210+
searchWithHighlighter(client, {"milvus users", "text search"});
211+
}
212+
213+
} // namespace
214+
215+
int
216+
main(int argc, char* argv[]) {
217+
printf("Example start...\n");
218+
219+
auto client = milvus::MilvusClientV2::Create();
220+
221+
milvus::ConnectParam connect_param{"http://localhost:19530", "root:Milvus"};
222+
auto status = client->Connect(connect_param);
223+
util::CheckStatus("connect milvus server", status);
224+
225+
runExample(client);
226+
227+
client->Disconnect();
228+
return 0;
229+
}

scripts/coverage.sh

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,40 @@ COVERAGE_OUTPUT_DIR="code_coverage"
2222
LCOV_CMD="lcov"
2323
LCOV_GEN_CMD="genhtml"
2424

25+
resolve_gcov_tool() {
26+
if [ -n "${GCOV_TOOL}" ] ; then
27+
echo "${GCOV_TOOL}"
28+
return 0
29+
fi
30+
31+
local compiler_file
32+
compiler_file=$(find "${ROOT_DIR}/${BUILD_OUTPUT_DIR}/CMakeFiles" -path '*/CMakeCXXCompiler.cmake' | head -n 1)
33+
if [ -n "${compiler_file}" ] ; then
34+
local compiler_version gcov_candidate
35+
compiler_version=$(grep 'set(CMAKE_CXX_COMPILER_VERSION "' "${compiler_file}" | sed -E 's/.*"([0-9]+)\..*/\1/' | head -n 1)
36+
if [ -n "${compiler_version}" ] ; then
37+
gcov_candidate="gcov-${compiler_version}"
38+
if command -v "${gcov_candidate}" >/dev/null 2>&1 ; then
39+
echo "${gcov_candidate}"
40+
return 0
41+
fi
42+
fi
43+
fi
44+
45+
echo "gcov"
46+
}
47+
2548
SOURCE="${BASH_SOURCE[0]}"
2649
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
2750
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
2851
SOURCE="$(readlink "$SOURCE")"
2952
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
3053
done
3154
ROOT_DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )"
55+
GCOV_TOOL="$(resolve_gcov_tool)"
3256

3357
echo "ROOT_DIR = ${ROOT_DIR}"
58+
echo "GCOV_TOOL = ${GCOV_TOOL}"
3459

3560
DIR_LCOV_OUTPUT="${ROOT_DIR}/${COVERAGE_OUTPUT_DIR}"
3661
DIR_GCNO="${ROOT_DIR}/${BUILD_OUTPUT_DIR}/src/"
@@ -44,22 +69,22 @@ rm -rf ${DIR_LCOV_OUTPUT}
4469
mkdir ${COVERAGE_OUTPUT_DIR}
4570

4671
# generate baseline (exclude proto-generated files)
47-
${LCOV_CMD} -c -i -d ${DIR_GCNO} -o ${FILE_INFO_BASE} \
72+
${LCOV_CMD} --gcov-tool "${GCOV_TOOL}" -c -i -d ${DIR_GCNO} -o ${FILE_INFO_BASE} \
4873
--exclude "*/_deps/*" --exclude "*.pb.cc" --exclude "*.grpc.pb.cc"
4974
if [ $? -ne 0 ]; then
5075
echo "Failed to generate coverage baseline"
5176
exit -1
5277
fi
5378

5479
# generate ut file (exclude proto-generated files)
55-
${LCOV_CMD} -c -d ${DIR_GCNO} -o ${FILE_INFO_UT} \
80+
${LCOV_CMD} --gcov-tool "${GCOV_TOOL}" -c -d ${DIR_GCNO} -o ${FILE_INFO_UT} \
5681
--exclude "*/_deps/*" --exclude "*.pb.cc" --exclude "*.grpc.pb.cc"
5782

5883
# merge baseline and ut file
59-
${LCOV_CMD} -a ${FILE_INFO_BASE} -a ${FILE_INFO_UT} -o ${FILE_INFO_COMBINE}
84+
${LCOV_CMD} --gcov-tool "${GCOV_TOOL}" -a ${FILE_INFO_BASE} -a ${FILE_INFO_UT} -o ${FILE_INFO_COMBINE}
6085

6186
# remove unnecessary info
62-
${LCOV_CMD} -r "${FILE_INFO_COMBINE}" -o "${FILE_INFO_OUTPUT}" \
87+
${LCOV_CMD} --gcov-tool "${GCOV_TOOL}" -r "${FILE_INFO_COMBINE}" -o "${FILE_INFO_OUTPUT}" \
6388
"/usr/*" \
6489
"*/install/*" \
6590
"*/src/include/nlohmann/*" \

src/impl/request/dql/SearchRequest.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,20 @@ SearchRequest::WithTimezone(const std::string& timezone) {
203203
return *this;
204204
}
205205

206+
const HighlighterPtr&
207+
SearchRequest::GetHighlighter() const {
208+
return highlighter_;
209+
}
210+
211+
void
212+
SearchRequest::SetHighlighter(const HighlighterPtr& highlighter) {
213+
highlighter_ = highlighter;
214+
}
215+
216+
SearchRequest&
217+
SearchRequest::WithHighlighter(const HighlighterPtr& highlighter) {
218+
highlighter_ = highlighter;
219+
return *this;
220+
}
221+
206222
} // namespace milvus

0 commit comments

Comments
 (0)