使用 Java 驅動程式和 Spring Data 進行 MongoDB Atlas 搜尋
一、簡介
在本教程中,我們將學習如何使用 Java MongoDB 驅動程式 API 來使用Atlas 搜尋功能。最後,我們將掌握建立查詢、對結果分頁和檢索元資訊。此外,我們還將介紹如何使用篩選器最佳化結果、調整結果分數以及選擇要顯示的特定欄位。
2. 場景和設定
MongoDB Atlas 有一個永久免費的集群,我們可以用它來測試所有功能。為了展示 Atlas Search 功能,我們只需要一個服務類別。我們將使用MongoTemplate
連接到我們的集合。
2.1.依賴關係
首先,要連接到 MongoDB,我們需要spring-boot-starter-data-mongodb :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
<version>3.1.2</version>
</dependency>
2.2.樣本資料集
在本教程中,我們將使用 MongoDB Atlas 的sample_mflix
範例資料集中的movies
集合來簡化範例。它包含自 1900 年代以來的電影數據,這將幫助我們展示 Atlas Search 的過濾功能。
2.3.使用動態映射建立索引
為了讓 Atlas Search 正常運作,我們需要索引。這些可以是靜態的或動態的。靜態索引有助於微調,而動態索引是一種出色的通用解決方案。那麼,讓我們從動態索引開始。
有幾種方法可以建立搜尋索引(包括以程式設計方式);我們將使用 Atlas UI。在那裡,我們可以透過從選單中存取Search
,選擇我們的集群,然後點擊Go to Atlas Search
來完成此操作:
點擊Create Search Index
後,我們將選擇 JSON Editor 來建立索引,然後按一下Next
:
最後,在下一個畫面上,我們選擇目標集合、索引名稱,並輸入索引定義:
{
"mappings": {
"dynamic": true
}
}
在本教程中,我們將使用名稱idx-queries
作為該索引。請注意,如果我們將索引命名default
,則在建立查詢時不需要指定其名稱。最重要的是,動態映射是更靈活、經常更改的模式的簡單選擇。
透過將mappings.dynamic
設為true
,Atlas Search會自動索引文件中所有動態可索引和支援的欄位類型。雖然動態映射提供了便利,尤其是當模式未知時,但與靜態映射相比,它們往往會消耗更多的磁碟空間,並且效率可能較低。
2.4.我們的電影搜尋服務
我們的範例將基於包含一些電影搜尋查詢的服務類,並從中提取有趣的資訊。我們將慢慢將它們建構成更複雜的查詢:
@Service
public class MovieAtlasSearchService {
private final MongoCollection<Document> collection;
public MovieAtlasSearchService(MongoTemplate mongoTemplate) {
MongoDatabase database = mongoTemplate.getDb();
this.collection = database.getCollection("movies");
}
// ...
}
我們所需要的只是對我們收集的未來方法的參考。
3. 建置查詢
Atlas Search 查詢是透過管道階段建立的,由List<Bson>
表示。最重要的階段是Aggregates.search()
,它接收SearchOperator
和可選的SearchOptions
物件。由於我們將索引稱為idx-queries
而不是default
,因此我們必須將其名稱包含在SearchOptions.searchOptions().index()
中。否則,我們將不會得到任何錯誤,也不會得到任何結果。
許多搜尋運算符可用於定義我們想要如何進行查詢。在此範例中,我們將使用SearchOperator.text()
按標籤尋找電影,該方法執行全文搜尋。我們將使用它透過 SearchPath.fieldPath() 搜尋fullplot
欄位的內容SearchPath.fieldPath().
為了方便閱讀,我們將省略靜態導入:
public Collection<Document> moviesByKeywords(String keywords) {
List<Bson> pipeline = Arrays.asList(
search(
text(
fieldPath("fullplot"), keywords
),
searchOptions()
.index("idx-queries")
),
project(fields(
excludeId(),
include("title", "year", "fullplot", "imdb.rating")
))
);
return collection.aggregate(pipeline)
.into(new ArrayList<>());
}
此外,我們pipeline
的第二階段是Aggregates.project()
,它代表投影。如果不指定,我們的查詢結果將包含我們文件中的所有欄位。但我們可以設定它並選擇我們想要(或不想要)出現在結果中的欄位。請注意,指定要包含的欄位會隱式排除除_id
欄位之外的所有其他欄位。因此,在這種情況下,我們排除_id
字段並傳遞我們想要的字段列表。請注意,我們也可以指定巢狀字段,例如imdb.rating
。
為了執行管道,我們對集合呼叫aggregate()
。這將傳回一個我們可以用來迭代結果的物件。最後,為了簡單起見,我們呼叫into()
來迭代結果並將它們加入我們傳回的集合。請注意,足夠大的集合可能會耗盡 JVM 中的記憶體。稍後我們將了解如何透過對結果進行分頁來消除這種擔憂。
最重要的是,管道階段順序很重要。如果我們將project()
階段放在search()
之前,我們會收到錯誤。
讓我們來看看在我們的服務上呼叫moviesByKeywords(“space cowboy”)
的前兩個結果:
[
{
"title": "Battle Beyond the Stars",
"fullplot": "Shad, a young farmer, assembles a band of diverse mercenaries in outer space to defend his peaceful planet from the evil tyrant Sador and his armada of aggressors. Among the mercenaries are Space Cowboy, a spacegoing truck driver from Earth; Gelt, a wealthy but experienced assassin looking for a place to hide; and Saint-Exmin, a Valkyrie warrior looking to prove herself in battle.",
"year": 1980,
"imdb": {
"rating": 5.4
}
},
{
"title": "The Nickel Ride",
"fullplot": "Small-time criminal Cooper manages several warehouses in Los Angeles that the mob use to stash their stolen goods. Known as \"the key man\" for the key chain he always keeps on his person that can unlock all the warehouses. Cooper is assigned by the local syndicate to negotiate a deal for a new warehouse because the mob has run out of storage space. However, Cooper's superior Carl gets nervous and decides to have cocky cowboy button man Turner keep an eye on Cooper.",
"year": 1974,
"imdb": {
"rating": 6.7
}
},
(...)
]
3.1.組合搜尋運算符
可以使用SearchOperator.compound()
組合搜尋運算子。在此範例中,我們將使用它來包含must
和should
子句。 must
子句包含一個或多個符合文件的條件。另一方面, should
子句包含我們希望結果包含的一個或多個條件。
這會改變分數,以便首先出現滿足這些條件的文檔:
public Collection<Document> late90sMovies(String keywords) {
List<Bson> pipeline = asList(
search(
compound()
.must(asList(
numberRange(
fieldPath("year"))
.gteLt(1995, 2000)
))
.should(asList(
text(
fieldPath("fullplot"), keywords
)
)),
searchOptions()
.index("idx-queries")
),
project(fields(
excludeId(),
include("title", "year", "fullplot", "imdb.rating")
))
);
return collection.aggregate(pipeline)
.into(new ArrayList<>());
}
我們保留了第一個查詢中相同的searchOptions()
和投影欄位。但是,這次,我們將text()
移至should
子句,因為我們希望關鍵字代表偏好,而不是要求。
然後,我們建立了一個must
子句,包括SearchOperator.numberRange(),
透過限制year
欄位的值,僅顯示 1995 年到 2000 年(不含)的影片。這樣,我們只返回那個時代的電影。
讓我們看看hacker assassin
的前兩個結果:
[
{
"title": "Assassins",
"fullplot": "Robert Rath is a seasoned hitman who just wants out of the business with no back talk. But, as things go, it ain't so easy. A younger, peppier assassin named Bain is having a field day trying to kill said older assassin. Rath teams up with a computer hacker named Electra to defeat the obsessed Bain.",
"year": 1995,
"imdb": {
"rating": 6.3
}
},
{
"fullplot": "Thomas A. Anderson is a man living two lives. By day he is an average computer programmer and by night a hacker known as Neo. Neo has always questioned his reality, but the truth is far beyond his imagination. Neo finds himself targeted by the police when he is contacted by Morpheus, a legendary computer hacker branded a terrorist by the government. Morpheus awakens Neo to the real world, a ravaged wasteland where most of humanity have been captured by a race of machines that live off of the humans' body heat and electrochemical energy and who imprison their minds within an artificial reality known as the Matrix. As a rebel against the machines, Neo must return to the Matrix and confront the agents: super-powerful computer programs devoted to snuffing out Neo and the entire human rebellion.",
"imdb": {
"rating": 8.7
},
"year": 1999,
"title": "The Matrix"
},
(...)
]
4. 對結果集進行評分
當我們使用search()
查詢文件時,結果會依照相關性順序顯示。這種相關性基於計算的分數,從最高到最低。這次,我們將修改late90sMovies()
以接收SearchScore
修飾符,以提高should
子句中情節關鍵字的相關性:
public Collection<Document> late90sMovies(String keywords, SearchScore modifier) {
List<Bson> pipeline = asList(
search(
compound()
.must(asList(
numberRange(
fieldPath("year"))
.gteLt(1995, 2000)
))
.should(asList(
text(
fieldPath("fullplot"), keywords
)
.score(modifier)
)),
searchOptions()
.index("idx-queries")
),
project(fields(
excludeId(),
include("title", "year", "fullplot", "imdb.rating"),
metaSearchScore("score")
))
);
return collection.aggregate(pipeline)
.into(new ArrayList<>());
}
此外,我們在欄位清單中包含metaSearchScore(“score”)
,以查看結果中每個文件的分數。例如,我們現在可以將「should」子句的相關性乘以imdb.votes
欄位的值,如下所示:
late90sMovies(
"hacker assassin",
SearchScore.boost(fieldPath("imdb.votes"))
)
這次,我們可以看到《駭客任務》排在了第一位,這要歸功於提升:
[
{
"fullplot": "Thomas A. Anderson is a man living two lives (...)",
"imdb": {
"rating": 8.7
},
"year": 1999,
"title": "The Matrix",
"score": 3967210.0
},
{
"fullplot": "(...) Bond also squares off against Xenia Onatopp, an assassin who uses pleasure as her ultimate weapon.",
"imdb": {
"rating": 7.2
},
"year": 1995,
"title": "GoldenEye",
"score": 462604.46875
},
(...)
]
4.1.使用評分函數
我們可以透過使用函數來改變結果的分數來實現更好的控制。讓我們向我們的方法傳遞一個函數,將year
欄位的值加到自然分數中。這樣,新電影最終會獲得更高的分數:
late90sMovies(keywords, function(
addExpression(asList(
pathExpression(
fieldPath("year"))
.undefined(1),
relevanceExpression()
))
));
程式碼以SearchScore.function()
開頭,它是SearchScoreExpression.addExpression()
因為我們需要add
操作。然後,由於我們想要從欄位新增值,因此我們使用SearchScoreExpression.pathExpression()
並指定我們想要的欄位: year
。此外,我們呼叫undefined()
來確定year
的後備值,以防它遺失。最後,我們呼叫relevanceExpression()
來傳回文件的相關性得分,該得分將會加到year
的值中。
當我們執行該命令時,我們將看到“The Matrix”現在首先出現,以及它的新樂譜:
[
{
"fullplot": "Thomas A. Anderson is a man living two lives (...)",
"imdb": {
"rating": 8.7
},
"year": 1999,
"title": "The Matrix",
"score": 2003.67138671875
},
{
"title": "Assassins",
"fullplot": "Robert Rath is a seasoned hitman (...)",
"year": 1995,
"imdb": {
"rating": 6.3
},
"score": 2003.476806640625
},
(...)
]
這對於定義在對結果進行評分時應具有更大權重的內容很有用。
5. 從元資料取得總行數
如果我們需要取得查詢中的結果總數,我們可以使用Aggregates.searchMeta()
而不是search()
來只檢索元資料資訊。使用此方法,不會傳回任何文件。因此,我們將使用它來計算 90 年代末也包含我們的關鍵字的電影數量。
為了進行有意義的過濾,我們還將在must
子句中包含keywords
:
public Document countLate90sMovies(String keywords) {
List<Bson> pipeline = asList(
searchMeta(
compound()
.must(asList(
numberRange(
fieldPath("year"))
.gteLt(1995, 2000),
text(
fieldPath("fullplot"), keywords
)
)),
searchOptions()
.index("idx-queries")
.count(total())
)
);
return collection.aggregate(pipeline)
.first();
}
這次, searchOptions()
包含對SearchOptions.count(SearchCount.total())
的調用,這確保我們獲得準確的總計數(而不是下限,下限更快,取決於集合大小)。另外,由於我們期望結果中只有一個對象,因此我們在aggregate()
上呼叫first()
。
最後,讓我們看看countLate90sMovies(“hacker assassin”)
回傳了什麼:
{
"count": {
"total": 14
}
}
這對於獲取有關我們的收藏的資訊非常有用,而無需在我們的結果中包含文件。
6. 注重結果
在 MongoDB Atlas Search 中, 分面查詢是一項功能,允許檢索有關搜尋結果的聚合和分類資訊。它幫助我們根據不同的標準分析和匯總數據,從而深入了解搜尋結果的分佈。
此外,它還可以將搜尋結果分組到不同的類別或儲存桶中,並檢索有關每個類別的計數或附加資訊。這有助於回答諸如「有多少文件與特定類別相符?」之類的問題。或“結果中某個欄位最常見的值是什麼?”
6.1.建立靜態索引
在我們的第一個範例中,我們將創建一個分面查詢,為我們提供有關 1900 年代以來電影類型的信息以及它們之間的關係。我們需要一個具有構面類型的索引,而使用動態索引時我們無法擁有該索引。
因此,我們首先在集合中建立一個新的搜尋索引,我們稱之為idx-facets
。請注意,我們將保持dynamic
為true
,這樣我們仍然可以查詢未明確定義的欄位:
{
"mappings": {
"dynamic": true,
"fields": {
"genres": [
{
"type": "stringFacet"
},
{
"type": "string"
}
],
"year": [
{
"type": "numberFacet"
},
{
"type": "number"
}
]
}
}
}
我們首先指定我們的映射不是動態的。然後,我們選擇我們感興趣的欄位來索引分面資訊。由於我們還想在查詢中使用篩選器,因此對於每個字段,我們指定標準類型的索引(如string
)和多麵類型之一(如stringFacet
)。
6.2.運行方面查詢
建立構面查詢涉及使用searchMeta()
並啟動SearchCollector.facet()
方法以包含我們的構面和用於過濾結果的運算子。定義構面時,我們必須選擇名稱並使用與我們建立的索引類型相對應的SearchFacet
方法。在我們的例子中,我們定義了一個stringFacet()
和一個numberFacet()
:
public Document genresThroughTheDecades(String genre) {
List pipeline = asList(
searchMeta(
facet(
text(
fieldPath("genres"), genre
),
asList(
stringFacet("genresFacet",
fieldPath("genres")
).numBuckets(5),
numberFacet("yearFacet",
fieldPath("year"),
asList(1900, 1930, 1960, 1990, 2020)
)
)
),
searchOptions()
.index("idx-facets")
)
);
return collection.aggregate(pipeline)
.first();
}
我們使用text()
運算子過濾特定類型的電影。由於影片通常包含多種類型,因此stringFacet()
也將顯示按頻率排名的五個(由numBuckets()
指定)相關類型。對於numberFacet()
,我們必須設定分隔聚合結果的邊界。我們至少需要兩個,最後一個是唯一的。
最後,我們只回傳第一個結果。讓我們看看如果我們按「恐怖」類型過濾的話會是什麼樣子:
{
"count": {
"lowerBound": 1703
},
"facet": {
"genresFacet": {
"buckets": [
{
"_id": "Horror",
"count": 1703
},
{
"_id": "Thriller",
"count": 595
},
{
"_id": "Drama",
"count": 395
},
{
"_id": "Mystery",
"count": 315
},
{
"_id": "Comedy",
"count": 274
}
]
},
"yearFacet": {
"buckets": [
{
"_id": 1900,
"count": 5
},
{
"_id": 1930,
"count": 47
},
{
"_id": 1960,
"count": 409
},
{
"_id": 1990,
"count": 1242
}
]
}
}
}
由於我們沒有指定總計數,因此我們得到了一個下限計數,後面是我們的方面名稱及其各自的儲存桶。
6.3.包括對結果進行分頁的分面階段
讓我們回到late90sMovies()
方法並在管道中包含$facet
階段。我們將使用它進行分頁和total rows
數。 search()
和project()
階段將維持不變:
public Document late90sMovies(int skip, int limit, String keywords) {
List<Bson> pipeline = asList(
search(
// ...
),
project(fields(
// ...
)),
facet(
new Facet("rows",
skip(skip),
limit(limit)
),
new Facet("totalRows",
replaceWith("$$SEARCH_META"),
limit(1)
)
)
);
return collection.aggregate(pipeline)
.first();
}
我們首先呼叫Aggregates.facet()
,它接收一個或多個面向。然後,我們實例化一個Facet
以包含Aggregates
類別中的skip()
和limit()
。雖然skip()
定義了我們的偏移量,但limit()
將限制檢索的文件數量。請注意,我們可以為我們的面命名任何我們喜歡的名稱。
另外,我們呼叫replaceWith(“ [$$SEARCH_META](https://www.mongodb.com/docs/atlas/atlas-search/query-syntax/#aggregation-variable/?utm_source=email&utm_campaign=java_influencer_baeldung&utm_medium=influencers) “)
來取得該欄位中的元資料資訊。最重要的是,為了讓每個結果的元資料資訊都不會重複,我們加入了limit(1)
。最後,當我們的查詢有元資料時,結果變成單一文件而不是數組,因此我們只傳回第一個結果。
七、結論
在本文中,我們了解了 MongoDB Atlas Search 如何為開發人員提供多功能且強大的工具集。將其與 Java MongoDB 驅動程式 API 整合可以增強搜尋功能、資料聚合和結果自訂。我們的實踐範例旨在提供對其功能的實際理解。無論是實現簡單的搜尋還是尋求複雜的資料分析,Atlas Search 都是 MongoDB 生態系統中的寶貴工具。
請記住利用索引、面向和動態映射的力量讓我們的資料為我們服務。與往常一樣,原始碼可以在 GitHub 上取得。