Skip to content

Commit

Permalink
feat: result grouping
Browse files Browse the repository at this point in the history
  • Loading branch information
sznowicki committed Dec 7, 2023
1 parent 86e43b5 commit 9a033de
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 60 deletions.
46 changes: 45 additions & 1 deletion dist/static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@

--color-background: oklch(1 0 0);
--color-base: oklch(0 0 0);
--color-contrast-bg: oklch(0 0 0);
--color-contrast-fg: oklch(1 0 0);
--border-size: 4px;
}

* {
box-sizing: border-box;
}

html, body {
font-family: var(--font-family-base);
width: 100%;
Expand All @@ -30,12 +36,41 @@ input[type="search"] {
}
}

.form-main {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
button {
padding: .75rem 0.5rem;
font-family: var(--font-family-head);
background: var(--color-contrast-bg);
color: var(--color-contrast-fg);
border: 2px solid var(--color-contrast-bg);
box-shadow: 0 0 0 4px var(--color-contrast-bg);

&:hover,
&:focus,
&:active {
border: 2px solid var(--color-contrast-fg);
}
}

h1, h2, h3, h4, h5 {
font-family: var(--font-family-head);
margin: 2em 0 1em;
margin: 3em 0 1em;
padding: 0;
}

h1 {
margin: 1em 0;
}

p {
margin: 1em 0;
}

ul {
margin: 1rem 0;
padding: 0;
Expand All @@ -62,3 +97,12 @@ blockquote {
max-width: 100%;
word-break: break-all;
}

.result-item-first-level {
& .result-item-first-level__sublist {
display: none;
}
& label:has(.result-item-first-level__checkbox:checked) ~ .result-item-first-level__sublist {
display: block;
}
}
36 changes: 19 additions & 17 deletions functions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,28 @@ export const onRequestGet = async (context) => {
docsPages,
] = await stats(env);
const doneIn = Date.now() - startTime;
const hasResults = result?.hits?.length > 0;

const hits = {};

result?.hits?.forEach((hit) => {
if (!hits[hit.index]) {
hits[hit.index] = {
label: hit.index === 'blogs' ? 'Blogs' : 'Docs',
items: [],
};
}

});

const hasBlogs = result?.hits.blogs.length > 0;
const hasDocs = result?.hits.docs.length > 0;
const results = [];
if (result.hits.blogs.length) {
results.push({
name: 'Blogs',
hits: result.hits.blogs,
});
}
if (result.hits.docs.length) {
results.push({
name: 'Docs',
hits: result.hits.docs,
});
}
const view = {
q,
title: 'kukei.eu',
hits,
hasResults,
noResults: q && !hasResults,
results,
hasQuery: !!q,
noResults: !(hasBlogs || hasDocs),
hasResults: results.length > 0,
doneIn,
hash,
blogPages,
Expand Down
78 changes: 46 additions & 32 deletions functions/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,54 @@ <h1>Kukey.eu <br > <span>curated search for web developers</span></h1>
<p>So far indexed {{ blogPages }} pages of blogs and {{ docsPages }} pages of docs.</p>
</div>
<form action="">
<input type="search" name="q" value="{{ q }}" aria-label="Search" placeholder="Type your query" />
<div class="form-main">
<input type="search" name="q" value="{{ q }}" aria-label="Search" placeholder="Type your query" autofocus />
<button type="submit">Search</button>
</div>
</form>
{{#hasResults}}
{{#hasQuery}}
<h2>Results</h2>
<p>Search results generated in {{ doneIn }}ms</p>
<section>
<h2>Blogs</h2>
<ul>
{{#blogs}}
<li>
<h3>{{ title }}</h3>
<p>{{ excerpt }}</p>
<blockquote>{{ highlight }}</blockquote>
<a href="{{ url }}">{{ url }}</a>
</li>
{{/blogs}}
</ul>
</section>
<section>
<h2>Docs</h2>
<ul>
{{#docs}}
<li>
<h3>{{ title }}</h3>
<p>{{ excerpt }}</p>
<blockquote>{{ highlight }}</blockquote>
<a href="{{ url }}">{{ url }}</a>
</li>
{{/docs}}
</ul>
</section>
{{/hasResults}}
{{#noResults}}
<p>No results found</p>
{{/noResults}}
{{#hasResults}}
{{#results}}
<section>
<h3>{{ name }}</h3>
<ul>
{{#hits}}
<li class="result-item-first-level">
<div class="result-item-first-level__data">
<h4>{{ title }}</h4>
<p>{{ excerpt }}</p>
<!-- <blockquote>{{ highlight }}</blockquote>-->
<a href="{{ url }}">{{ url }}</a>
</div>
{{#subItems.length}}
<label>
<input type="checkbox" class="result-item-first-level__checkbox" />
Show more from {{hostname}} ({{ subItems.length }})
</label>
<ul class="result-item-first-level__sublist">
{{#subItems}}
<li class="result-item-second-level">
<h4>{{ title }}</h4>
<p>{{ excerpt }}</p>
<blockquote>{{ highlight }}</blockquote>
<a href="{{ url }}">{{ url }}</a>
</li>
{{/subItems}}
</ul>
{{/subItems.length}}
</li>
{{/hits}}
</ul>
</section>
{{/results}}
{{/hasResults}}
{{#noResults}}
<p>No results found</p>
{{/noResults}}
{{/hasQuery}}

</main>
</body>
</html>
96 changes: 86 additions & 10 deletions lib/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,44 +10,120 @@ const getMeiliClient = (env) => {
const makeOptions = (p) => ({
attributesToHighlight: ['content'],
attributesToCrop: ['content'],
facets: ['hostname'],
facets: ['hostname', 'lang'],
cropLength: 50,
limit: 10,
offset: p,
});

const doSearch = async (result, env, index, q, p) => {
/**
* @typedef {Object} ResultItem
* @property {string} url
* @property {string} highlight
* @property {string} index
* @property {string} excerpt
* @property {string} title
* @property {string} lang
* @property {string} hostname
*/
/**
* @typedef {Object} ResultGrouped
* @extends {ResultItem}
* @property {ResultItem[]} subItems
*/

/**
*
* @param results
* @param env
* @param index
* @param q
* @param p
* @returns {Promise<void>}
*/
const doSearch = async (results, env, index, q, p) => {
const meiliClient = getMeiliClient(env);
const results = await meiliClient.index(index).search(q, makeOptions(p));
const searchResult = await meiliClient.index(index).search(q, makeOptions(p));

const {
facetDistribution,
hits
} = results;
} = searchResult;

result.facets.push(...facetDistribution);
Object.keys(facetDistribution).forEach((key) => {
const facetData = facetDistribution[key];
if (!results.facets[key]) {
results.facets[key] = [];
}
Object.keys(facetData).forEach((facetKey) => {
results.facets[key].push({
count: facetData[facetKey],
value: facetKey,
});
});
});
/**
*
* @typedef {Map<string, {ResultGrouped}>}
*/
const originsMap = new Map();

hits.forEach((el) => {
const highlightRaw = el._formatted?.content ?? '';
// remove excessive whitespace
const highlight = highlightRaw.replace(/\s+/g, ' ');

result.hits.push({
const final = {
url: el.url,
highlight,
index,
excerpt: el.excerpt,
title: el.title,
});
hostname: el.hostname,
};

const indexArr = results.hits[final.index];
let existingOrigin = originsMap.get(el.hostname);

if (existingOrigin) {
existingOrigin.subItems.push(final);
return;
}

final.subItems = [];
originsMap.set(el.hostname, final);
indexArr.push(final);
});
};

/**
* @typedef {Object} Facet
* @property {string} name
* @property {Array<{count: number, value: string}>} data
*/
/**
* @typedef {Object} SearchResult
* @property {Array<Facet>} facets
* @property {Object<string, Array<{ResultGrouped}>>} hits
*/
/**
*
* @param {Object} env
* @param {string} q
* @param {number} p
* @returns {Promise<{SearchResult}>}
*/
export const search = async (env, q, p = 0) => {
const result = {
facets: [],
hists: [],
hits: {
'blogs': [],
'docs': [],
},
};
await doSearch(env, 'blogs', q, p);
await doSearch(env, 'docs', q, p);

await doSearch(result, env, 'blogs', q, p);
await doSearch(result, env, 'docs', q, p);

return result;
};
Expand Down

0 comments on commit 9a033de

Please sign in to comment.