Fixed implementation

This commit is contained in:
byte[] 2021-12-29 19:52:15 -05:00
parent 25e9739383
commit a60fe1c48c
3 changed files with 85 additions and 91 deletions

View file

@ -137,9 +137,9 @@ function listenAutocomplete() {
if (localAc !== null && 'ac' in event.target.dataset) { if (localAc !== null && 'ac' in event.target.dataset) {
inputField = event.target; 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); return showAutocomplete(suggestions, originalTerm, event.target);
} }

View file

@ -10,6 +10,30 @@
* @property {number[]} associations * @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. * See lib/philomena/autocomplete.ex for binary structure details.
* *
@ -34,9 +58,9 @@ export class LocalAutocompleter {
/** @type {number} */ /** @type {number} */
this.referenceStart = this.view.getUint32(backingStore.byteLength - 8, true); this.referenceStart = this.view.getUint32(backingStore.byteLength - 8, true);
/** @type {number} */ /** @type {number} */
this.formatVersion = this.view.getUint32(backingStore.byteLength - 12, true); this.secondaryStart = this.referenceStart + 8 * this.numTags;
/** @type {number} */ /** @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) { if (this.formatVersion !== 2) {
throw new Error('Incompatible autocomplete format version'); throw new Error('Incompatible autocomplete format version');
@ -72,11 +96,11 @@ export class LocalAutocompleter {
*/ */
getResultAt(i) { getResultAt(i) {
const nameLocation = this.view.getUint32(this.referenceStart + i * 8, true); 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 // This is actually an alias, so follow it
return this.getResultAt(imageCount & ~(1 << 31)); return this.getResultAt(-imageCount);
} }
const [ name, associations ] = this.getTagFromLocation(nameLocation); const [ name, associations ] = this.getTagFromLocation(nameLocation);
@ -91,21 +115,51 @@ export class LocalAutocompleter {
* @returns {Result} * @returns {Result}
*/ */
getSecondaryResultAt(i) { 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); 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 * @param {(i: number) => Result} getResult
* @returns {string} * @param {(name: string) => number} compare
* @param {{[key: string]: Result}} results
*/ */
nameInNamespace(s) { scanResults(getResult, compare, results) {
const v = s.split(':', 2); let min = 0;
let max = this.numTags;
if (v.length === 2) return v[1]; /** @type {number[]} */
return v[0]; //@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[]} * @returns {Result[]}
*/ */
topK(prefix, k) { topK(prefix, k) {
/** @type {Result[]} */ /** @type {{[key: string]: Result}} */
const results = []; const results = {};
/** @type {number[]} */
//@ts-expect-error No type for window.booru yet
const hiddenTags = window.booru.hiddenTagList;
if (prefix === '') { if (prefix === '') {
return results; return [];
} }
// Binary search to find last smaller prefix // Find normally, in full name-sorted order
let l = 0; const prefixMatch = (/** @type {string} */ name) => strcmp(name.slice(0, prefix.length), prefix);
let r = this.numTags; this.scanResults(this.getResultAt.bind(this), prefixMatch, results);
while (l < r - 1) { // Find in secondary order
const m = (l + (r - l) / 2) | 0; const namespaceMatch = (/** @type {string} */ name) => strcmp(nameInNamespace(name).slice(0, prefix.length), prefix);
const { name } = this.getResultAt(m); this.scanResults(this.getSecondaryResultAt.bind(this), namespaceMatch, results);
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);
}
}
// Sort results by image count // 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);
} }
} }

View file

@ -61,9 +61,8 @@ defmodule Philomena.Autocomplete do
ac_file = int32_align(ac_file) ac_file = int32_align(ac_file)
reference_start = byte_size(ac_file) reference_start = byte_size(ac_file)
size_of_reference = 8
reference_locations = reference_indexes =
tags tags
|> Enum.with_index() |> Enum.with_index()
|> Enum.map(fn {name, index} -> {name, index} end) |> Enum.map(fn {name, index} -> {name, index} end)
@ -74,11 +73,11 @@ defmodule Philomena.Autocomplete do
pos = Map.fetch!(name_locations, name) pos = Map.fetch!(name_locations, name)
if not is_nil(alias_target) do if not is_nil(alias_target) do
target = Map.fetch!(reference_locations, alias_target) target = Map.fetch!(reference_indexes, alias_target)
<<references::binary, pos::32-little, 1::1, target::31-little>> <<references::binary, pos::32-little, -target::32-little>>
else else
<<references::binary, pos::32-little, 0::1, images_count::31-little>> <<references::binary, pos::32-little, images_count::32-little>>
end end
end) end)
@ -92,10 +91,9 @@ defmodule Philomena.Autocomplete do
secondary_references = secondary_references =
tags tags
|> Enum.map(&{name_in_namespace(elem(&1, 0)), &1}) |> Enum.map(&{name_in_namespace(elem(&1, 0)), &1})
|> Enum.uniq_by(fn {k, _v} -> k end)
|> Enum.sort() |> Enum.sort()
|> Enum.reduce(<<>>, fn {_k, v}, secondary_references -> |> Enum.reduce(<<>>, fn {_k, v}, secondary_references ->
target = Map.fetch!(reference_locations, v) target = Map.fetch!(reference_indexes, v)
<<secondary_references::binary, target::32-little>> <<secondary_references::binary, target::32-little>>
end) end)