Tìm kiếm mờ trong Javascript có ý nghĩa


98

Tôi đang tìm thư viện JavaScript tìm kiếm mờ để lọc một mảng. Tôi đã thử sử dụng fuzzyset.jsfuse.js , nhưng kết quả thật tệ (có những bản demo bạn có thể thử trên các trang được liên kết).

Sau khi thực hiện một số đọc về khoảng cách Levenshtein, tôi thấy nó là một con số gần đúng kém về những gì người dùng đang tìm kiếm khi họ nhập. Đối với những người không biết, hệ thống sẽ tính toán số lần chèn , xóathay thế cần thiết để làm cho hai chuỗi khớp nhau.

Một lỗ hổng rõ ràng, được sửa trong mô hình Levenshtein-Demerau, là cả blubboob đều được coi là tương tự như bóng đèn (mỗi loại yêu cầu hai thay thế). Tuy nhiên, rõ ràng là bóng đèn đó giống với blub hơn là boob , và mô hình tôi vừa đề cập nhận ra điều đó bằng cách cho phép chuyển vị .

Tôi muốn sử dụng điều này trong ngữ cảnh hoàn thành văn bản, vì vậy nếu tôi có một mảng ['international', 'splint', 'tinder']và truy vấn của tôi là int , tôi nghĩ quốc tế nên xếp hạng cao hơn splint , mặc dù trước đó có điểm (cao hơn = tệ hơn) là 10 so với 3 của sau.

Vì vậy, những gì tôi đang tìm kiếm (và sẽ tạo nếu nó không tồn tại), là một thư viện thực hiện những việc sau:

  • Trọng số các thao tác văn bản khác nhau
  • Trọng số của mỗi thao tác khác nhau tùy thuộc vào vị trí chúng xuất hiện trong một từ (thao tác sớm tốn kém hơn thao tác muộn)
  • Trả về danh sách kết quả được sắp xếp theo mức độ liên quan

Có ai đi qua bất cứ điều gì như thế này? Tôi nhận ra rằng StackOverflow không phải là nơi để yêu cầu các đề xuất phần mềm, nhưng ẩn ý (không còn nữa!) Ở trên là: tôi đang nghĩ về điều này có đúng cách không?


Biên tập

Tôi đã tìm thấy một bài báo tốt (pdf) về chủ đề này. Một số ghi chú và đoạn trích:

Các hàm chỉnh sửa khoảng cách liên kết ấn định chi phí tương đối thấp hơn cho một chuỗi các lần chèn hoặc xóa

hàm khoảng cách Monger-Elkan (Monge & Elkan 1996), là một biến thể affine của hàm khoảng cách Smith-Waterman (Durban et al. 1998) với các tham số chi phí cụ thể

Đối với khoảng cách Smith-Waterman (wikipedia) , "Thay vì xem xét chuỗi tổng, thuật toán Smith – Waterman so sánh các đoạn của tất cả các độ dài có thể và tối ưu hóa độ đo tương tự." Đó là cách tiếp cận n-gram.

Một số liệu tương tự rộng rãi, không dựa trên mô hình khoảng cách chỉnh sửa, là số liệu Jaro (Jaro 1995; 1989; Winkler 1999). Trong tài liệu liên kết bản ghi, kết quả tốt đã thu được khi sử dụng các biến thể của phương pháp này, dựa trên số lượng và thứ tự của các ký tự chung giữa hai chuỗi.

Một biến thể của điều này do Winkler (1999) cũng sử dụng độ dài P của tiền tố chung dài nhất

(dường như chủ yếu dành cho các chuỗi ngắn)

Đối với mục đích hoàn thành văn bản, các phương pháp Monger-Elkan và Jaro-Winkler dường như có ý nghĩa nhất. Sự bổ sung của Winkler vào chỉ số Jaro có hiệu quả làm tăng trọng số của các từ bắt đầu. Và khía cạnh liên kết của Monger-Elkan có nghĩa là sự cần thiết phải hoàn thành một từ (đơn giản là một chuỗi các phép bổ sung) sẽ không làm mất tác dụng của nó quá nhiều.

Phần kết luận:

xếp hạng TFIDF hoạt động tốt nhất trong số một số chỉ số khoảng cách dựa trên mã thông báo và chỉ số khoảng cách chỉnh sửa affine-gap do Monge và Elkan đề xuất hoạt động tốt nhất trong số một số chỉ số khoảng cách chỉnh sửa chuỗi. Một thước đo khoảng cách tốt đáng ngạc nhiên là một sơ đồ heuristic nhanh, được đề xuất bởi Jaro và sau đó được Winkler mở rộng. Điều này hoạt động gần giống như lược đồ Monge-Elkan, nhưng là một thứ tự cường độ nhanh hơn. Một cách đơn giản để kết hợp phương pháp TFIDF và Jaro-Winkler là thay thế các khớp mã thông báo chính xác được sử dụng trong TFIDF bằng các khớp mã thông báo gần đúng dựa trên lược đồ Jaro-Winkler. Sự kết hợp này hoạt động tốt hơn một chút so với Jaro-Winkler hoặc TFIDF ở mức trung bình, và đôi khi hoạt động tốt hơn nhiều. Nó cũng gần về hiệu suất với sự kết hợp đã học của một số số liệu tốt nhất được xem xét trong bài báo này.


Câu hỏi tuyệt vời. Tôi đang tìm cách làm điều gì đó tương tự, nhưng với các cân nhắc so sánh chuỗi tương tự. Bạn đã bao giờ tìm thấy / xây dựng một triển khai javascript của các so sánh chuỗi của bạn chưa? Cảm ơn.
nicholas

1
@nicholas Tôi chỉ đơn giản chia fuzzyset.js trên github để giải thích cho các chuỗi truy vấn nhỏ hơn và mặc dù nó không tính đến các thao tác chuỗi có trọng số, nhưng kết quả khá tốt cho ứng dụng hoàn thành chuỗi dự kiến ​​của tôi. Xem repo
willlma

Cảm ơn. Tôi sẽ thử nó. Tôi cũng tìm thấy hàm so sánh chuỗi này: github.com/zdyn/jaro-winkler-js . Có vẻ cũng hoạt động khá tốt.
nicholas


1
@michaelday Điều đó không tính đến lỗi chính tả. Trong bản demo, việc nhập krolekhông trở lại Final Fantasy V: Krile, mặc dù tôi rất muốn. Nó yêu cầu tất cả các ký tự trong truy vấn phải có cùng thứ tự trong kết quả, điều này khá thiển cận. Có vẻ như cách duy nhất để tìm kiếm mờ tốt là có một cơ sở dữ liệu về các lỗi chính tả phổ biến.
willlma

Câu trả lời:


21

Câu hỏi hay! Nhưng suy nghĩ của tôi là, thay vì cố gắng sửa đổi Levenshtein-Demerau, bạn có thể tốt hơn nên thử một thuật toán khác hoặc kết hợp / cân nhắc kết quả từ hai thuật toán.

Tôi ngạc nhiên rằng các kết quả khớp chính xác hoặc gần giống với "tiền tố bắt đầu" là điều mà Levenshtein-Demerau không có trọng lượng cụ thể - nhưng kỳ vọng của người dùng rõ ràng của bạn sẽ.

Tôi đã tìm kiếm "tốt hơn Levenshtein" và trong số những thứ khác, tìm thấy điều này:

http://www.joyofdata.de/blog/comparison-of-string-distance-algorithm/

Điều này đề cập đến một số biện pháp "khoảng cách chuỗi". Ba thứ có vẻ đặc biệt phù hợp với yêu cầu của bạn, sẽ là:

  1. Khoảng cách chuỗi con chung dài nhất: Số ký hiệu tối thiểu phải được xóa trong cả hai chuỗi cho đến khi các chuỗi con kết quả giống hệt nhau.

  2. Khoảng cách q-gram: Tổng chênh lệch tuyệt đối giữa các vectơ N-gram của cả hai chuỗi.

  3. Khoảng cách Jaccard: 1 phút là thương số của N-gram dùng chung và tất cả N-gram quan sát được.

Có thể bạn có thể sử dụng kết hợp có trọng số (hoặc tối thiểu) của các số liệu này, với Levenshtein - chuỗi con chung, N-gram chung hoặc Jaccard đều sẽ rất thích các chuỗi tương tự - hoặc có thể thử chỉ sử dụng Jaccard?

Tùy thuộc vào kích thước danh sách / cơ sở dữ liệu của bạn, các thuật toán này có thể đắt vừa phải. Đối với một tìm kiếm mờ mà tôi đã triển khai, tôi đã sử dụng số lượng N-gram có thể định cấu hình làm "khóa truy xuất" từ DB sau đó chạy thước đo khoảng cách chuỗi đắt tiền để sắp xếp chúng theo thứ tự ưu tiên.

Tôi đã viết một số ghi chú về Tìm kiếm chuỗi mờ trong SQL. Xem:


63

Tôi đã thử sử dụng các thư viện mờ hiện có như fuse.js và cũng thấy chúng rất tệ, vì vậy tôi đã viết một thư viện hoạt động về cơ bản giống như tìm kiếm siêu phàm. https://github.com/farzher/fuzzysort

Lỗi chính tả duy nhất mà nó cho phép là chuyển đoạn. Nó khá chắc chắn (1k sao, 0 vấn đề) , rất nhanh và xử lý trường hợp của bạn dễ dàng:

fuzzysort.go('int', ['international', 'splint', 'tinder'])
// [{highlighted: '*int*ernational', score: 10}, {highlighted: 'spl*int*', socre: 3003}]


4
Tôi không hài lòng với Fuse.js và đã dùng thử thư viện của bạn - hoạt động rất tốt! Làm tốt lắm :)
dave

1
Vấn đề duy nhất với thư viện này mà tôi phải đối mặt là khi từ là hoàn thành nhưng đánh vần sai ví dụ, nếu từ đúng là "XRP" và nếu tôi đã tìm kiếm "XRT" nó không cho tôi một điểm
PirateApp

1
@PirateApp vâng, tôi không xử lý lỗi chính tả (vì tìm kiếm của sublime không có). Tôi đang nhìn vào điều này bây giờ mà mọi người đang phàn nàn. bạn có thể cung cấp cho tôi với trường hợp ví dụ sử dụng khi tìm kiếm này không thành công như một vấn đề github
Farzher

3
Đối với những người bạn đang thắc mắc về lib này, nó hiện cũng đã được triển khai kiểm tra chính tả! Tôi khuyên bạn nên lib này qua fusejs và những người khác
PirateApp

1
@ user4815162342 bạn phải tự viết mã. hãy kiểm tra chuỗi này, nó có một mẫu mã github.com/farzher/fuzzysort/issues/19
Farzher

19

Đây là một kỹ thuật tôi đã sử dụng một vài lần ... Nó cho kết quả khá tốt. Không làm mọi thứ bạn yêu cầu mặc dù. Ngoài ra, điều này có thể tốn kém nếu danh sách quá lớn.

get_bigrams = (string) ->
    s = string.toLowerCase()
    v = new Array(s.length - 1)
    for i in [0..v.length] by 1
        v[i] = s.slice(i, i + 2)
    return v

string_similarity = (str1, str2) ->
    if str1.length > 0 and str2.length > 0
        pairs1 = get_bigrams(str1)
        pairs2 = get_bigrams(str2)
        union = pairs1.length + pairs2.length
        hit_count = 0
        for x in pairs1
            for y in pairs2
                if x is y
                    hit_count++
        if hit_count > 0
            return ((2.0 * hit_count) / union)
    return 0.0

Chuyển hai chuỗi vào string_similarityđó sẽ trả về một số giữa 01.0tùy thuộc vào mức độ giống nhau của chúng. Ví dụ này sử dụng Lo-Dash

Ví dụ sử dụng ....

query = 'jenny Jackson'
names = ['John Jackson', 'Jack Johnson', 'Jerry Smith', 'Jenny Smith']

results = []
for name in names
    relevance = string_similarity(query, name)
    obj = {name: name, relevance: relevance}
    results.push(obj)

results = _.first(_.sortBy(results, 'relevance').reverse(), 10)

console.log results

Ngoài ra .... có một tay chơi

Đảm bảo rằng bảng điều khiển của bạn đang mở hoặc bạn sẽ không nhìn thấy bất kỳ thứ gì :)


3
Cảm ơn, đó chính xác là những gì tôi đang tìm kiếm. Nó sẽ chỉ là tốt hơn nếu nó là js đồng bằng;)
lucaswxp

1
function get_bigrams (string) {var s = string.toLowerCase () var v = s.split (''); for (var i = 0; i <v.length; i ++) {v [i] = s.slice (i, i + 2); } return v; } function string_similarity (str1, str2) {if (str1.length> 0 && str2.length> 0) {var pair1 = get_bigrams (str1); var cặp2 = get_bigrams (str2); var union = pair1.length + pair2.length; var hits = 0; for (var x = 0; x <pair1.length; x ++) {for (var y = 0; y <pair2.length; y ++) {if (pair1 [x] == pair2 [y]) hit_count ++; }} if (hits> 0) return ((2.0 * hits) / union); } trả về 0.0}
jaya

Làm thế nào để sử dụng điều này trong các đối tượng mà bạn sẽ muốn tìm kiếm trong một số khóa?
user3808307

Điều này có một số vấn đề: 1) Nó làm giảm trọng số của các ký tự ở đầu và cuối chuỗi. 2) Các phép so sánh bigram là O (n ^ 2). 3) Điểm tương tự có thể trên 1 do cách thực hiện. Điều này rõ ràng là không có ý nghĩa. Tôi khắc phục tất cả những vấn đề này trong câu trả lời của tôi bên dưới.
MgSam

8

đây là hàm ngắn và gọn của tôi cho kết hợp mờ:

function fuzzyMatch(pattern, str) {
  pattern = '.*' + pattern.split('').join('.*') + '.*';
  const re = new RegExp(pattern);
  return re.test(str);
}

Mặc dù có lẽ không phải những gì bạn muốn trong hầu hết các trường hợp, nhưng nó chính xác là dành cho tôi.
schmijos


2

Cập nhật tháng 11 năm 2019. Tôi thấy cầu chì có một số nâng cấp khá tốt. Tuy nhiên, tôi không thể sử dụng bool's (nghĩa là toán tử OR, AND, v.v.) cũng như không thể sử dụng giao diện tìm kiếm API để lọc kết quả.

Tôi đã phát hiện ra nextapps-de/flexsearch: https://github.com/nextapps-de/flexsearch và tôi tin rằng nó vượt xa rất nhiều thư viện tìm kiếm javascript khác mà tôi đã thử và nó có hỗ trợ boollọc tìm kiếm & phân trang.

Bạn có thể nhập danh sách các đối tượng javascript cho dữ liệu tìm kiếm của mình (tức là bộ nhớ) và API được ghi lại khá tốt: https://github.com/nextapps-de/flexsearch#api-overview

Cho đến nay, tôi đã lập chỉ mục gần 10.000 bản ghi và các tìm kiếm của tôi gần như ngay lập tức; tức là lượng thời gian không đáng kể cho mỗi lần tìm kiếm.


2

đây là giải pháp được cung cấp bởi @InternalFX, nhưng trong JS (tôi đã sử dụng nó nên chia sẻ):

function get_bigrams(string){
  var s = string.toLowerCase()
  var v = s.split('');
  for(var i=0; i<v.length; i++){ v[i] = s.slice(i, i + 2); }
  return v;
}

function string_similarity(str1, str2){
  if(str1.length>0 && str2.length>0){
    var pairs1 = get_bigrams(str1);
    var pairs2 = get_bigrams(str2);
    var union = pairs1.length + pairs2.length;
    var hits = 0;
    for(var x=0; x<pairs1.length; x++){
      for(var y=0; y<pairs2.length; y++){
        if(pairs1[x]==pairs2[y]) hits++;
    }}
    if(hits>0) return ((2.0 * hits) / union);
  }
  return 0.0
}

2

Tôi đã khắc phục sự cố với giải pháp Bigram CoffeeScript bởi InternalFx và biến nó thành giải pháp n-gram chung (bạn có thể tùy chỉnh kích thước của gram).

Đây là TypeScript nhưng bạn có thể xóa các chú thích kiểu và nó hoạt động tốt như JavaScript vani.

/**
 * Compares the similarity between two strings using an n-gram comparison method. 
 * The grams default to length 2.
 * @param str1 The first string to compare.
 * @param str2 The second string to compare.
 * @param gramSize The size of the grams. Defaults to length 2.
 */
function stringSimilarity(str1: string, str2: string, gramSize: number = 2) {
  function getNGrams(s: string, len: number) {
    s = ' '.repeat(len - 1) + s.toLowerCase() + ' '.repeat(len - 1);
    let v = new Array(s.length - len + 1);
    for (let i = 0; i < v.length; i++) {
      v[i] = s.slice(i, i + len);
    }
    return v;
  }

  if (!str1?.length || !str2?.length) { return 0.0; }

  //Order the strings by length so the order they're passed in doesn't matter 
  //and so the smaller string's ngrams are always the ones in the set
  let s1 = str1.length < str2.length ? str1 : str2;
  let s2 = str1.length < str2.length ? str2 : str1;

  let pairs1 = getNGrams(s1, gramSize);
  let pairs2 = getNGrams(s2, gramSize);
  let set = new Set<string>(pairs1);

  let total = pairs2.length;
  let hits = 0;
  for (let item of pairs2) {
    if (set.delete(item)) {
      hits++;
    }
  }
  return hits / total;
}

Ví dụ:

console.log(stringSimilarity("Dog", "Dog"))
console.log(stringSimilarity("WolfmanJackIsDaBomb", "WolfmanJackIsDaBest"))
console.log(stringSimilarity("DateCreated", "CreatedDate"))
console.log(stringSimilarity("a", "b"))
console.log(stringSimilarity("CreateDt", "DateCreted"))
console.log(stringSimilarity("Phyllis", "PyllisX"))
console.log(stringSimilarity("Phyllis", "Pylhlis"))
console.log(stringSimilarity("cat", "cut"))
console.log(stringSimilarity("cat", "Cnut"))
console.log(stringSimilarity("cc", "Cccccccccccccccccccccccccccccccc"))
console.log(stringSimilarity("ab", "ababababababababababababababab"))
console.log(stringSimilarity("a whole long thing", "a"))
console.log(stringSimilarity("a", "a whole long thing"))
console.log(stringSimilarity("", "a non empty string"))
console.log(stringSimilarity(null, "a non empty string"))

Hãy thử nó trong Sân chơi TypeScript


0
(function (int) {
    $("input[id=input]")
        .on("input", {
        sort: int
    }, function (e) {
        $.each(e.data.sort, function (index, value) {
          if ( value.indexOf($(e.target).val()) != -1 
              && value.charAt(0) === $(e.target).val().charAt(0) 
              && $(e.target).val().length === 3 ) {
                $("output[for=input]").val(value);
          };
          return false
        });
        return false
    });
}(["international", "splint", "tinder"]))

jsfiddle http://jsfiddle.net/guest271314/QP7z5/


0

Kiểm tra tiện ích bổ sung Google Trang tính của tôi có tên Flookup và sử dụng chức năng này:

Flookup (lookupValue, tableArray, lookupCol, indexNum, threshold, [rank])

Thông số chi tiết là:

  1. lookupValue: giá trị bạn đang tìm kiếm
  2. tableArray: bảng bạn muốn tìm kiếm
  3. lookupCol: cột bạn muốn tìm kiếm
  4. indexNum: cột bạn muốn dữ liệu được trả về
  5. threshold: tỷ lệ phần trăm tương tự bên dưới dữ liệu không được trả lại
  6. rank: trận đấu hay nhất thứ n (tức là nếu trận đấu đầu tiên không theo ý bạn)

Điều này sẽ đáp ứng yêu cầu của bạn ... mặc dù tôi không chắc chắn về điểm số 2.

Tìm hiểu thêm tại trang web chính thức .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.