Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Novo algoritmo de busca #200

Merged
merged 8 commits into from
Oct 10, 2024
101 changes: 101 additions & 0 deletions assets/js/levenshtein.js
levxyca marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Levenshtein algorithm by Gustaf Andersson: https://github.com/gustf/js-levenshtein
function _min(d0, d1, d2, bx, ay)
{
return d0 < d1 || d2 < d1
? d0 > d2
? d2 + 1
: d0 + 1
: bx === ay
? d1
: d1 + 1;
}

export function levenshtein(a, b)
{
if (a === b) {
return 0;
}

if (a.length > b.length) {
var tmp = a;
a = b;
b = tmp;
}

var la = a.length;
var lb = b.length;

while (la > 0 && (a.charCodeAt(la - 1) === b.charCodeAt(lb - 1))) {
la--;
lb--;
}

var offset = 0;

while (offset < la && (a.charCodeAt(offset) === b.charCodeAt(offset))) {
offset++;
}

la -= offset;
lb -= offset;

if (la === 0 || lb < 3) {
return lb;
}

var x = 0;
var y;
var d0;
var d1;
var d2;
var d3;
var dd;
var dy;
var ay;
var bx0;
var bx1;
var bx2;
var bx3;

var vector = [];

for (y = 0; y < la; y++) {
vector.push(y + 1);
vector.push(a.charCodeAt(offset + y));
}

var len = vector.length - 1;

for (; x < lb - 3;) {
bx0 = b.charCodeAt(offset + (d0 = x));
bx1 = b.charCodeAt(offset + (d1 = x + 1));
bx2 = b.charCodeAt(offset + (d2 = x + 2));
bx3 = b.charCodeAt(offset + (d3 = x + 3));
dd = (x += 4);
for (y = 0; y < len; y += 2) {
dy = vector[y];
ay = vector[y + 1];
d0 = _min(dy, d0, d1, bx0, ay);
d1 = _min(d0, d1, d2, bx1, ay);
d2 = _min(d1, d2, d3, bx2, ay);
dd = _min(d2, d3, dd, bx3, ay);
vector[y] = dd;
d3 = d2;
d2 = d1;
d1 = d0;
d0 = dy;
}
}

for (; x < lb;) {
bx0 = b.charCodeAt(offset + (d0 = x));
dd = ++x;
for (y = 0; y < len; y += 2) {
dy = vector[y];
vector[y] = dd = _min(dy, d0, dd, bx0, vector[y + 1]);
d0 = dy;
}
}

return dd;
}
162 changes: 142 additions & 20 deletions assets/js/script.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import "./dark_mode.js";
import { levenshtein } from "./levenshtein.js";

const exactWordScore = 12;
const partialWordScore = 10;
const levenshteinScore = 10;
const levenshteinThreshold = 3;

const searchInput = document.querySelector("#search-input");
const cardsSection = document.querySelector("#cards");
const filterSelect = document.querySelector("#tags-filter");
let listOfCardsFiltered = [];
let favoriteCards = [];

const starIcon = "https://img.icons8.com/ios/50/star--v1.png";
const starIconFilled =
"https://img.icons8.com/ios-glyphs/30/ffe100/star--v1.png";
Expand Down Expand Up @@ -52,30 +59,145 @@ function filterCards() {
searchCards();
}

function sortCards(sortingArray) {
if (listOfCardsFiltered.length > 0) {
if (!Array.isArray(sortingArray) || !sortingArray.length) {
const cards = document.querySelector("#cards");
// selects all cards that are not hidden and sorts them by title
// every child is re-appended to cards in the order of the now sorted array. When an element is re-appended it is actually moved from its previous location
[...cards.querySelectorAll(".card:not([style*='display: none;'])")]
.sort((a, b) => a.querySelector(".card__title").textContent.toLowerCase().localeCompare(b.querySelector(".card__title").textContent.toLowerCase()))
.forEach(node => cards.appendChild(node));
} else {
const cards = document.querySelector("#cards");
// selects all cards that are not hidden and sorts them by the order of the sortingArray
// every child is re-appended to cards in the order of the now sorted array. When an element is re-appended it is actually moved from its previous location
[...cards.querySelectorAll(".card:not([style*='display: none;'])")]
.sort((a, b) => sortingArray.indexOf(a) - sortingArray.indexOf(b))
.forEach(node => cards.appendChild(node));
}
}
}

function searchCards() {
const inputValue = searchInput.value.toLowerCase();
let cardsFiltered = [];
const inputValue = searchInput.value.toLowerCase().trim();
let cardsScores = [];

for (const card of listOfCardsFiltered) {
const cardContent = card.textContent.toLowerCase();
if (inputValue.length > 0) {
const searchWords = inputValue.split(/\s+/);

if (cardContent.includes(inputValue)){
card.style.display = "";
cardsFiltered.push(card);
for (const card of listOfCardsFiltered) {
let cardScore = 0;

// search for words inside the title that either contains the search words or have a low levenshtein distance
// only consider the best case for each search word
const cardTitle = card.querySelector(".card__title").textContent.toLowerCase();
const titleWords = cardTitle.split(/\s+/);
let titleScore = 0;

searchWords.forEach((searchWord) => {
let wordScore = 0;

titleWords.some((word) => {
if (word == searchWord) {
// breaks the loop if the word is an exact match, since no other word can have a higher score
wordScore = exactWordScore;
return true;

} else if (wordScore < partialWordScore) {
if (word.includes(searchWord)) {
wordScore = partialWordScore;

} else if (word.length > 3) {
const levenshteinDistance = levenshtein(searchWord, word);

// only the word with the lowest levenshtein distance will be considered
if ((levenshteinDistance <= levenshteinThreshold) && (levenshteinScore - levenshteinDistance > wordScore)) {
wordScore = levenshteinScore - levenshteinDistance;
}
}
}
});

titleScore += wordScore;
});

// give extra points for words in title
cardScore += titleScore * 10;

// search for words inside the description that either contains the search words or have a low levenshtein distance
// only consider the best case for each search word
const cardDescription = card.querySelector(".card__description").textContent.toLowerCase();
const descriptionWords = cardDescription.split(/\s+/);
let descriptionScore = 0;

searchWords.forEach((searchWord) => {
let wordScore = 0;

descriptionWords.some((word) => {
if (word == searchWord) {
// breaks the loop if the word is an exact match, since no other word can have a higher score
wordScore = exactWordScore;
return true;

} else if (wordScore < partialWordScore) {
if (word.includes(searchWord)) {
wordScore = partialWordScore;

} else if (word.length > 3) {
const levenshteinDistance = levenshtein(searchWord, word);

// only the word with the lowest levenshtein distance will be considered
if ((levenshteinDistance <= levenshteinThreshold) && (levenshteinScore - levenshteinDistance > wordScore)) {
wordScore = levenshteinScore - levenshteinDistance;
}
}
}
});

descriptionScore += wordScore;
});

cardScore += descriptionScore;

if (cardScore > 0) {
card.style.display = "";
cardsScores.push([card, cardScore]);
} else {
card.style.display = "none";
}
}

const msgNotFound = document.querySelector("div.msg");

if (cardsScores.length > 0) {
msgNotFound.style.display = "none";
// sort the array of cards by score
cardsScores.sort((a, b) => b[1] - a[1]);
// remove the scores from the array
cardsScores = cardsScores.map((card) => card[0]);
sortCards(cardsScores);
} else {
card.style.display = "none";
msgNotFound.style.display = "";
}
}

const msgNotFound = document.querySelector("div.msg");
msgNotFound.style.display = cardsFiltered.length==0 ? "" : "none";

} else {
// display all cards if search input is empty
for (const card of listOfCardsFiltered) {
card.style.display = "";
cardsScores.push(card);
}

const msgNotFound = document.querySelector("div.msg");
msgNotFound.style.display = "none";
sortCards();
}
}

function insertCardsIntoHtml(data) {
let cards = `<div class="msg">
<div class=collumn-1>
<img src="assets/img/no-results-found.png" alt="Mulher olhando para site sem dados" />
<img src="assets/img/no-results-found.png" alt="Mulher olhando para site sem dados" />
<a href="https://storyset.com/data">Data illustrations by Storyset</a>
</div>
<div class=collumn-2>
Expand Down Expand Up @@ -220,12 +342,12 @@ function generateCardId(defaultCardId, title, description) {
* @returns {string} The hashed representation of the content.
*/
function generateContentId(title = '', description = '', hash = 5381) {
const data = (title + description).slice(0, 32).split(' ').join('')
const data = (title + description).slice(0, 32).split(' ').join('')

for (let i = 0; i < data.length; i++) {
hash = ((hash << 5) + hash) + data.charCodeAt(i);
}
for (let i = 0; i < data.length; i++) {
hash = ((hash << 5) + hash) + data.charCodeAt(i);
}

const hashString = Math.abs(hash).toString(36); // Convert to base-36 string
return hashString;
}
const hashString = Math.abs(hash).toString(36); // Convert to base-36 string
return hashString;
}