MongoDB 全文和部分文本搜索

100

环境:

  • MongoDB(3.2.0)与Mongoose

集合:

  • 用户

文本索引创建:

  BasicDBObject keys = new BasicDBObject();
  keys.put("name","text");

  BasicDBObject options = new BasicDBObject();
  options.put("name", "userTextSearch");
  options.put("unique", Boolean.FALSE);
  options.put("background", Boolean.TRUE);
  
  userCollection.createIndex(keys, options); // using MongoTemplate

文档:

  • {"name":"LEONEL"}

查询:

  • db.users.find( { "$text" : { "$search" : "LEONEL" } } ) => 找到
  • db.users.find( { "$text" : { "$search" : "leonel" } } ) => 找到(搜索区分大小写为false)
  • db.users.find( { "$text" : { "$search" : "LEONÉL" } } ) => 找到(搜索区分变音符号为false)
  • db.users.find( { "$text" : { "$search" : "LEONE" } } ) => 找到(部分匹配搜索)
  • db.users.find( { "$text" : { "$search" : "LEO" } } ) => 没有找到(部分匹配搜索)
  • db.users.find( { "$text" : { "$search" : "L" } } ) => 没有找到(部分匹配搜索)

为什么使用"LEO"或"L"作为查询条件会得到0个结果呢?

不允许在文本索引搜索中使用正则表达式。

db.getCollection('users')
     .find( { "$text" : { "$search" : "/LEO/i", 
                          "$caseSensitive": false, 
                          "$diacriticSensitive": false }} )
     .count() // 0 results

db.getCollection('users')
     .find( { "$text" : { "$search" : "LEO", 
                          "$caseSensitive": false, 
                          "$diacriticSensitive": false }} )
.count() // 0 results

MongoDB文档:


12
此问题涉及使用文本索引进行部分搜索和大小写不敏感搜索。请不要标记此问题为重复的,@LucasCosta。 - Leonel
你尝试过使用/LEO/i吗?你可以在MongoDB中使用正则表达式来搜索值。 - BrTkCa
@LucasCosta 的文本索引搜索不允许使用正则表达式。 - Leonel
无索引搜索: https://dev59.com/gVsW5IYBdhLWcg3wSVqM#48250561 - TomoMiha
11个回答

89
截至MongoDB 3.4版本,文本搜索功能旨在支持基于语言特定的停用词和词干提取算法的文本内容不区分大小写的搜索。支持语言的词干提取规则基于标准算法,通常可以处理常见动词和名词,但不能处理专有名词。
该功能没有显式支持部分匹配或模糊匹配,但可对同一结果进行词干提取的术语可能会产生匹配效果。例如:"taste"、"tastes"和"tasteful"都可被提取为"tast"。您可以尝试使用Snowball Stemming Demo页面来实验更多单词和提取算法。
如果您想进行高效的部分匹配,需要采用不同的方法。以下链接提供了一些有用的思路: MongoDB问题跟踪器中还有一个相关的改进请求:SERVER-15090:改进文本索引以支持部分单词匹配,您可以观看/投票支持。

1
现在有更好的方法。请检查免费层中的Atlas搜索以提高效率:https://docs.atlas.mongodb.com/atlas-search/ - Nice-Guy
1
抱歉,但Atlas只是一种不同的解决方案。当然,我们可以使用Elastic,对于那些需要自托管解决方案的人来说,它甚至更好。但无论如何,Atlas都不能解决MongoDB无法进行子字符串搜索的问题... - pva

27

如果不创建索引,我们可以直接使用:

db.users.find({ name: /<full_or_partial_text>/i})(不区分大小写)


点赞,它可以与aqp一起使用。谢谢! - Gaëtan Boyals
12
new RegExp(string, 'i') 适用于任何需要动态字符串搜索的人。 - Dominus Vilicus
我该如何在其中设置一个变量? - tony Macias
24
请注意,这种方法不够高效且无法扩展,因为搜索不是在一个已索引的字段上进行的。对于大型表格,这种方法会很慢。 - imekinox
这只是在“name”列中进行搜索。 - Ashkan

25

由于当前Mongo默认不支持部分搜索......

我创建了一个简单的静态方法。

import mongoose from 'mongoose'

const PostSchema = new mongoose.Schema({
    title: { type: String, default: '', trim: true },
    body: { type: String, default: '', trim: true },
});

PostSchema.index({ title: "text", body: "text",},
    { weights: { title: 5, body: 3, } })

PostSchema.statics = {
    searchPartial: function(q, callback) {
        return this.find({
            $or: [
                { "title": new RegExp(q, "gi") },
                { "body": new RegExp(q, "gi") },
            ]
        }, callback);
    },

    searchFull: function (q, callback) {
        return this.find({
            $text: { $search: q, $caseSensitive: false }
        }, callback)
    },

    search: function(q, callback) {
        this.searchFull(q, (err, data) => {
            if (err) return callback(err, data);
            if (!err && data.length) return callback(err, data);
            if (!err && data.length === 0) return this.searchPartial(q, callback);
        });
    },
}

export default mongoose.models.Post || mongoose.model('Post', PostSchema)

如何使用:

import Post from '../models/post'

Post.search('Firs', function(err, data) {
   console.log(data);
})

@LeventeOrbán 承诺!我将在下面放置一个答案。 - flash
更多信息请参见:https://docs.mongodb.com/manual/reference/operator/query/text/index.html - https://docs.mongodb.com/manual/text-search/index.html - Ricardo Canelas
让我们谈论一下性能问题。如果我有100万条记录,需要多长时间? - Stunner
谢谢!如何获取分数权重? - Djb
很好的回答,但是你如何处理部分搜索中的变音符号? :)) - Alex Totolici
显示剩余3条评论

20

如果您想使用MongoDB全文搜索的所有优势,并希望进行部分匹配(例如自动完成),则Shrikant Prabhu提到的基于n-gram的方法对我来说是正确的解决方案。显然,当索引大量文档时,这可能不太实用。

在我的情况下,我主要需要部分匹配仅适用于我的文档中的title字段(和一些其他短字段)。

我使用了一个边缘n-gram方法。这是什么意思?简而言之,您将像"密西西比河"这样的字符串转换为像"Mis Miss Missi Missis Mississ Mississi Mississip Mississipp Mississippi Riv Rive River"这样的字符串。

受Liu Gen的此代码启发,我想出了这种方法:

function createEdgeNGrams(str) {
    if (str && str.length > 3) {
        const minGram = 3
        const maxGram = str.length
        
        return str.split(" ").reduce((ngrams, token) => {
            if (token.length > minGram) {   
                for (let i = minGram; i <= maxGram && i <= token.length; ++i) {
                    ngrams = [...ngrams, token.substr(0, i)]
                }
            } else {
                ngrams = [...ngrams, token]
            }
            return ngrams
        }, []).join(" ")
    } 
    
    return str
}

let res = createEdgeNGrams("Mississippi River")
console.log(res)

现在要在Mongo中使用这个功能,我需要给我的文档添加一个searchTitle字段,并通过上述函数将实际的title字段转换为边缘n-gram以设置它的值。我还为searchTitle字段创建了一个"text"索引。

然后,我通过使用投影来排除searchTitle字段的搜索结果:

db.collection('my-collection')
  .find({ $text: { $search: mySearchTerm } }, { projection: { searchTitle: 0 } })

3
在我看来,这是目前为止最好的解决方案,可惜Mongo没有开箱即用的ngram。 - lucaswxp
它返回的结果不够准确。例如,“大号”会包括:“格雷森·拉斯”。老实说,Mongo对于文本搜索完全无用,最好使用像ElasticSearch这样的辅助数据库。 - Oliver Dixon
@OliverDixon 真的吗?太疯狂了。为什么 large 包括 Grayson Lars?它是否认为这是一个音形匹配? - Johannes Fahrenkrug
@JohannesFahrenkrug 我不知道,但这是我们测试数据基于n-grams方法所显示的 :-/ - Oliver Dixon

6

我在这里将@Ricardo Canelas的答案封装成了一个mongoose插件 on npm

做出了两个更改: - 使用了promises - 可以搜索任何类型为String的字段

这是重要的源代码:

// mongoose-partial-full-search

module.exports = exports = function addPartialFullSearch(schema, options) {
  schema.statics = {
    ...schema.statics,
    makePartialSearchQueries: function (q) {
      if (!q) return {};
      const $or = Object.entries(this.schema.paths).reduce((queries, [path, val]) => {
        val.instance == "String" &&
          queries.push({
            [path]: new RegExp(q, "gi")
          });
        return queries;
      }, []);
      return { $or }
    },
    searchPartial: function (q, opts) {
      return this.find(this.makePartialSearchQueries(q), opts);
    },

    searchFull: function (q, opts) {
      return this.find({
        $text: {
          $search: q
        }
      }, opts);
    },

    search: function (q, opts) {
      return this.searchFull(q, opts).then(data => {
        return data.length ? data : this.searchPartial(q, opts);
      });
    }
  }
}

exports.version = require('../package').version;

使用方法

// PostSchema.js
import addPartialFullSearch from 'mongoose-partial-full-search';
PostSchema.plugin(addPartialFullSearch);

// some other file.js
import Post from '../wherever/models/post'

Post.search('Firs').then(data => console.log(data);)

2
如果你正在使用一个变量来存储要搜索的字符串或值:
它可以与正则表达式一起使用,例如:
{ collection.find({ name of Mongodb field: new RegExp(variable_name, 'i') }

这里的 I 代表忽略大小写选项


我正在使用Monk,集合只是db.get()函数,用于连接数据库。 - vigviswa

2

2
我为您提供的快速且有效的解决方案是:首先使用文本搜索,如果没有找到,则使用正则表达式进行另一个查询。如果您不想进行两个查询,$or也可以使用,但是需要对所有查询字段进行索引

此外,最好不要使用大小写不敏感的正则表达式,因为它不能依赖于索引。在我的情况下,我复制了所使用字段的小写副本。


1

我创建了一个额外的字段,将文档中我想要搜索的所有字段组合在一起。然后我只是使用正则表达式:

user = {
    firstName: 'Bob',
    lastName: 'Smith',
    address: {
        street: 'First Ave',
        city: 'New York City',
        }
    notes: 'Bob knows Mary'
}

// add combined search field with '+' separator to preserve spaces
user.searchString = `${user.firstName}+${user.lastName}+${user.address.street}+${user.address.city}+${user.notes}`

db.users.find({searchString: {$regex: 'mar', $options: 'i'}})
// returns Bob because 'mar' matches his notes field

// TODO write a client-side function to highlight the matching fragments

0

在“纯”Meteor项目中使用MongodB进行全/部分搜索

我将Flash的代码适配到了Meteor-Collections和simpleSchema上,但没有使用mongoose(也就是说,删除了.plugin()方法和schema.path(虽然在Flash的代码中看起来像是simpleSchema属性,但对我来说并没有解决)),并返回结果数组而不是游标。

我认为这可能会帮助某些人,所以我分享一下。

export function partialFullTextSearch(meteorCollection, searchString) {

    // builds an "or"-mongoDB-query for all fields with type "String" with a regEx as search parameter
    const makePartialSearchQueries = () => {
        if (!searchString) return {};
        const $or = Object.entries(meteorCollection.simpleSchema().schema())
            .reduce((queries, [name, def]) => {
                def.type.definitions.some(t => t.type === String) &&
                queries.push({[name]: new RegExp(searchString, "gi")});
                return queries
            }, []);
        return {$or}
    };

    // returns a promise with result as array
    const searchPartial = () => meteorCollection.rawCollection()
        .find(makePartialSearchQueries(searchString)).toArray();

    // returns a promise with result as array
    const searchFull = () => meteorCollection.rawCollection()
        .find({$text: {$search: searchString}}).toArray();

    return searchFull().then(result => {
        if (result.length === 0) throw null
        else return result
    }).catch(() => searchPartial());

}

这将返回一个Promise,因此应该像这样调用它(即作为服务器端异步Meteor-Method searchContact的返回值)。这意味着在调用此方法之前,您已经将simpleSchema附加到了您的集合上。

return partialFullTextSearch(Contacts, searchString).then(result => result);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接