mirror of
https://github.com/philomena-dev/philomena.git
synced 2025-01-20 06:37: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) {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue