疯狂检查JS Word建议植入

时间:2019-01-11 22:45:05

标签: javascript search

来源

这是我们产品搜索功能的拼写检查器类型功能。

我尝试了Fuse.js,但速度变慢了。 我尝试了FuzzySort.js,但速度不够快,不够准确。

问题

输入用户文本,并从一组可能的单词中获取有关用户键入内容的建议。这些建议应产生分数以帮助显示相关性。由于最终的单词列表包含大约50,000个独特单词,因此我们只希望将最高的5分作为建议。

速度需要在单个内核上并且在50,000字以内的100毫秒内发生。

我们只有100Mb的可用内存

问题示例

单词列表:“苹果”,“香蕉”,“杏”

输入:“ epp”

在此示例中,权重为重物。

输出:“苹果”得分:.8,“杏子”得分:.3,“香蕉”得分:0

方法

我们可以通过指向单词本身的位置来绘制每个单词的所有字符的图形。通过这样做,我们可以获取输入并遍历此图,并拉出与输入中每个字符的位置相匹配的所有单词,然后将匹配项相加以获得分数。

示例

单词列表:“苹果”,“香蕉”,“杏”

{ a: {
       0: ['apple', 'apricot'], 
       1: ['banana'],
       3: ['banana'],
       5: ['banana']
     },
  b: {
       0: ['banana']
     },
  c: {
       4: ['apricot']
     }
  ....
}

作为字符,'a'在'apple'中,而'apricot'在索引0 Graph ['a'] [0]中将返回['apple','apricot']

因此,用户键入'epp'时,索引0中没有带'e'的单词,因此[]返回,但是索引1中的'p'返回['apple','杏子']和'p'索引2返回['apple']

因此返回的值是[],['apple','apricot'],['apple'],我们可以将它们合并在一起,并计算产生分数的每个单词的出现。

实现

在测试时,我得到了30毫秒的时间范围,并根据输入的长度添加了一个阈值,以丢弃不符合相关标准的单词。

class SpellChecker {
    constructor () {
        this.options = {
        limit : 5, // How many words can be returned,
        threshold: 0.7 // 1 complete match, 0 complete mismatch
        },
        this.characterGraph = {}
    }
    search (text) {
        const characters = text.split('')
        let words = []

        // Get words from graph
        _.forEach(characters, (character, index) => {
            words = words.concat(this.characterGraph[character][index] || [])
        })

        // Group words and get score
        let group = {}
        _.forEach(words, (word) => {
            if (!group[word]) group[word] = 0
            group[word]++
        })

        // Get the highest score words
        const highestScore = []
        for (let index = 0; index < this.options.limit; index++) {
            highestScore.push({ score: -Infinity })
        }
        let lowest = _.minBy(highestScore, 'score').score

        for(let word in group) {
            const count = group[word]
            if (count > lowest) {
                highestScore[_.findIndex(highestScore, ['score', lowest])] = {
                    score: count,
                    target: word
                }
                lowest = _.minBy(highestScore, 'score').score
            }
        }

        // Filter if score is less the relevant and return
        return _.filter(highestScore, (word) => word.score >= Math.round(text.length * this.options.threshold))
    }
    init (words) {
        _.forEach(words, (word) => {
            const characters = word.split('')
            _.forEach(characters, (character, index) => {
                this.characterGraph[character] = this.characterGraph[character] || {}
                this.characterGraph[character][index] = this.characterGraph[character][index] || []
                this.characterGraph[character][index].push(word)
            })
        })
    }
}

const spellChecker = new SpellChecker()

// Create Graph
spellChecker.init(['apple', 'banana', 'apricot'])

// Search
const result = spellChecker.search('epp')

// Print
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>

问题

此方法解决了速度问题,但要牺牲比赛的准确性。

准确性问题示例

单词列表:“苹果”,“香蕉”,“杏”

输入:“ eapricot”

从理论上讲,“杏”应该是最佳匹配,但在这种情况下,它将获得0分。

我能想到的最好的方法是循环处理一个单词范围。

范围示例

输入:“ eapricot” 范围:1

过程:['apricot','eapricot','eapricot']删除并在单词中添加1个字符,可以看到用户是否按位置离开,但这在我看来是个糟糕的解决方案。

有什么想法吗?

0 个答案:

没有答案