mirror of
https://github.com/philomena-dev/philomena.git
synced 2025-01-19 22:27:59 +01:00
Fixed implementation
This commit is contained in:
parent
25e9739383
commit
a60fe1c48c
3 changed files with 85 additions and 91 deletions
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
<<references::binary, pos::32-little, 1::1, target::31-little>>
|
||||
<<references::binary, pos::32-little, -target::32-little>>
|
||||
else
|
||||
<<references::binary, pos::32-little, 0::1, images_count::31-little>>
|
||||
<<references::binary, pos::32-little, images_count::32-little>>
|
||||
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)
|
||||
|
||||
<<secondary_references::binary, target::32-little>>
|
||||
end)
|
||||
|
|
Loading…
Reference in a new issue