From a60fe1c48cee5b72ca31d57796572f46cf245a8b Mon Sep 17 00:00:00 2001 From: "byte[]" Date: Wed, 29 Dec 2021 19:52:15 -0500 Subject: [PATCH] Fixed implementation --- assets/js/autocomplete.js | 4 +- assets/js/utils/local-autocompleter.js | 160 ++++++++++++------------- lib/philomena/autocomplete.ex | 12 +- 3 files changed, 85 insertions(+), 91 deletions(-) diff --git a/assets/js/autocomplete.js b/assets/js/autocomplete.js index 4bb3f4a9..58b8f23f 100644 --- a/assets/js/autocomplete.js +++ b/assets/js/autocomplete.js @@ -137,9 +137,9 @@ function listenAutocomplete() { if (localAc !== null && 'ac' in event.target.dataset) { inputField = event.target; - originalTerm = inputField.value; + originalTerm = `${inputField.value}`.toLowerCase(); - const suggestions = localAc.topK(inputField.value, 5).map(({ name, imageCount }) => ({ label: `${name} (${imageCount})`, value: name })); + const suggestions = localAc.topK(originalTerm, 5).map(({ name, imageCount }) => ({ label: `${name} (${imageCount})`, value: name })); return showAutocomplete(suggestions, originalTerm, event.target); } diff --git a/assets/js/utils/local-autocompleter.js b/assets/js/utils/local-autocompleter.js index 2bf5321b..7f9a9ee4 100644 --- a/assets/js/utils/local-autocompleter.js +++ b/assets/js/utils/local-autocompleter.js @@ -10,6 +10,30 @@ * @property {number[]} associations */ +/** + * Compare two strings, C-style. + * + * @param {string} a + * @param {string} b + * @returns {number} + */ +function strcmp(a, b) { + return a < b ? -1 : Number(a > b); +} + +/** + * Returns the name of a tag without any namespace component. + * + * @param {string} s + * @returns {string} + */ +function nameInNamespace(s) { + const v = s.split(':', 2); + + if (v.length === 2) return v[1]; + return v[0]; +} + /** * See lib/philomena/autocomplete.ex for binary structure details. * @@ -34,9 +58,9 @@ export class LocalAutocompleter { /** @type {number} */ this.referenceStart = this.view.getUint32(backingStore.byteLength - 8, true); /** @type {number} */ - this.formatVersion = this.view.getUint32(backingStore.byteLength - 12, true); + this.secondaryStart = this.referenceStart + 8 * this.numTags; /** @type {number} */ - this.numSecondary = (backingStore.byteLength - this.referenceStart - this.numTags * 8 - 12) / 4; + this.formatVersion = this.view.getUint32(backingStore.byteLength - 12, true); if (this.formatVersion !== 2) { throw new Error('Incompatible autocomplete format version'); @@ -72,11 +96,11 @@ export class LocalAutocompleter { */ getResultAt(i) { const nameLocation = this.view.getUint32(this.referenceStart + i * 8, true); - const imageCount = this.view.getUint32(this.referenceStart + i * 8 + 4, true); + const imageCount = this.view.getInt32(this.referenceStart + i * 8 + 4, true); - if (imageCount >>> 31 & 1) { + if (imageCount < 0) { // This is actually an alias, so follow it - return this.getResultAt(imageCount & ~(1 << 31)); + return this.getResultAt(-imageCount); } const [ name, associations ] = this.getTagFromLocation(nameLocation); @@ -91,21 +115,51 @@ export class LocalAutocompleter { * @returns {Result} */ getSecondaryResultAt(i) { - const referenceIndex = this.view.getUint32(this.referenceStart + this.numTags * 8 + i * 4); + const referenceIndex = this.view.getUint32(this.secondaryStart + i * 4, true); return this.getResultAt(referenceIndex); } /** - * Returns the name of a tag without any namespace component. + * Perform a binary search to fetch all results matching a condition. * - * @param {string} s - * @returns {string} + * @param {(i: number) => Result} getResult + * @param {(name: string) => number} compare + * @param {{[key: string]: Result}} results */ - nameInNamespace(s) { - const v = s.split(':', 2); + scanResults(getResult, compare, results) { + let min = 0; + let max = this.numTags; - if (v.length === 2) return v[1]; - return v[0]; + /** @type {number[]} */ + //@ts-expect-error No type for window.booru yet + const hiddenTags = window.booru.hiddenTagList; + + while (min < max - 1) { + const med = (min + (max - min) / 2) | 0; + const { name } = getResult(med); + + if (compare(name) >= 0) { + // too large, go left + max = med; + } + else { + // too small, go right + min = med; + } + } + + // Scan forward until no more matches occur + while (min < this.numTags - 1) { + const result = getResult(++min); + if (compare(result.name) !== 0) { + break; + } + + // Add if no associations are filtered + if (hiddenTags.findIndex(ht => result.associations.includes(ht)) === -1) { + results[result.name] = result; + } + } } /** @@ -116,82 +170,24 @@ export class LocalAutocompleter { * @returns {Result[]} */ topK(prefix, k) { - /** @type {Result[]} */ - const results = []; - - /** @type {number[]} */ - //@ts-expect-error No type for window.booru yet - const hiddenTags = window.booru.hiddenTagList; + /** @type {{[key: string]: Result}} */ + const results = {}; if (prefix === '') { - return results; + return []; } - // Binary search to find last smaller prefix - let l = 0; - let r = this.numTags; + // Find normally, in full name-sorted order + const prefixMatch = (/** @type {string} */ name) => strcmp(name.slice(0, prefix.length), prefix); + this.scanResults(this.getResultAt.bind(this), prefixMatch, results); - while (l < r - 1) { - const m = (l + (r - l) / 2) | 0; - const { name } = this.getResultAt(m); - - if (name.slice(0, prefix.length) >= prefix) { - // too large, go left - r = m; - } - else { - // too small, go right - l = m; - } - } - - // Scan forward until no more matches occur - while (l < this.numTags - 1) { - const result = this.getResultAt(++l); - if (!result.name.startsWith(prefix)) { - break; - } - - // Add if no associations are filtered - if (hiddenTags.findIndex(ht => result.associations.includes(ht)) === -1) { - results.push(result); - } - } - - // Binary search again to find in secondary list - l = 0; - r = this.numSecondary; - - while (l < r - 1) { - const m = (l + (r - l) / 2) | 0; - const { name } = this.getSecondaryResultAt(m); - - if (this.nameInNamespace(name).slice(0, prefix.length) >= prefix) { - // too large, go left - r = m; - } - else { - // too small, go right - l = m; - } - } - - // Scan forward until no more matches occur - while (l < this.numSecondary - 1) { - const result = this.getSecondaryResultAt(++l); - if (!this.nameInNamespace(result.name).startsWith(prefix)) { - break; - } - - // Add if no associations are filtered - if (hiddenTags.findIndex(ht => result.associations.includes(ht)) === -1) { - results.push(result); - } - } + // Find in secondary order + const namespaceMatch = (/** @type {string} */ name) => strcmp(nameInNamespace(name).slice(0, prefix.length), prefix); + this.scanResults(this.getSecondaryResultAt.bind(this), namespaceMatch, results); // Sort results by image count - results.sort((a, b) => b.imageCount - a.imageCount); + const sorted = Object.values(results).sort((a, b) => b.imageCount - a.imageCount); - return results.slice(0, k); + return sorted.slice(0, k); } } diff --git a/lib/philomena/autocomplete.ex b/lib/philomena/autocomplete.ex index cbb1a48b..15ca3e07 100644 --- a/lib/philomena/autocomplete.ex +++ b/lib/philomena/autocomplete.ex @@ -61,9 +61,8 @@ defmodule Philomena.Autocomplete do ac_file = int32_align(ac_file) reference_start = byte_size(ac_file) - size_of_reference = 8 - reference_locations = + reference_indexes = tags |> Enum.with_index() |> Enum.map(fn {name, index} -> {name, index} end) @@ -74,11 +73,11 @@ defmodule Philomena.Autocomplete do pos = Map.fetch!(name_locations, name) if not is_nil(alias_target) do - target = Map.fetch!(reference_locations, alias_target) + target = Map.fetch!(reference_indexes, alias_target) - <> + <> else - <> + <> end end) @@ -92,10 +91,9 @@ defmodule Philomena.Autocomplete do secondary_references = tags |> Enum.map(&{name_in_namespace(elem(&1, 0)), &1}) - |> Enum.uniq_by(fn {k, _v} -> k end) |> Enum.sort() |> Enum.reduce(<<>>, fn {_k, v}, secondary_references -> - target = Map.fetch!(reference_locations, v) + target = Map.fetch!(reference_indexes, v) <> end)