Skip to content

Commit

Permalink
Improve flexibility of additional search queries in autocomplete (#407)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jade-GG authored Jan 29, 2024
1 parent 71d9be7 commit a66c245
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 49 deletions.
11 changes: 11 additions & 0 deletions config/rapidez/frontend.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@
'default' => ['cart', 'login', 'credentials', 'payment', 'success'],
],

'autocomplete' => [
// Attach additional indexes to the autocomplete
// Uses the views in rapidez::layouts.partials.header.autocomplete
'additionals' => [
'categories' => ['name^3', 'description'],
],

'debounce' => 100,
'size' => 10,
],

// Link store codes to theme folders
// The structure is `'store_code' => 'folder_path'`
'themes' => [
Expand Down
74 changes: 57 additions & 17 deletions resources/js/components/Search/Autocomplete.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,43 +12,75 @@ export default {
type: Number,
default: 100,
},
},
mounted() {
this.$nextTick(() => this.$emit('mounted'))
size: {
type: Number,
default: 10,
},
multiMatchTypes: {
type: Array,
default: () => ['best_fields', 'phrase', 'phrase_prefix'],
},
},
render() {
return this.$scopedSlots.default(Object.assign(this, { self: this }))
return this.$scopedSlots.default(this)
},
data() {
return {
results: { count: 0 },
results: {},
resultsCount: 0,
searchAdditionals: () => null,
}
},
methods: {
searchAdditionals(query) {
if (!this.additionals) {
mounted() {
this.$nextTick(() => this.$emit('mounted'))
let self = this
// Define function here to gain access to the debounce prop
this.searchAdditionals = useDebounceFn(function (query) {
if (!self.additionals) {
return
}
this.results = { count: 0 }
// Initialize with empty data to preserve additionals order
self.results = Object.fromEntries(Object.keys(self.additionals).map((indexName) => [indexName, []]))
self.resultsCount = 0
let url = new URL(config.es_url)
let auth = `Basic ${btoa(`${url.username}:${url.password}`)}`
let baseUrl = url.origin
Object.entries(this.additionals).forEach(([name, fields]) => {
Object.entries(self.additionals).forEach(([name, data]) => {
let fields = data['fields'] ?? data
let size = data['size'] ?? self.size ?? undefined
let sort = data['sort'] ?? undefined
let multimatch = self.multiMatchTypes.map((type) => ({
multi_match: {
query: query,
type: type,
fields: fields,
fuzziness: type.includes('phrase') ? undefined : 'AUTO',
},
}))
let esQuery = {
size: size,
sort: sort,
query: {
multi_match: {
query: query,
fields: fields,
fuzziness: 'AUTO',
bool: {
should: multimatch,
minimum_should_match: 1,
},
},
highlight: {
pre_tags: ['<mark>'],
post_tags: ['</mark>'],
fields: Object.fromEntries(fields.map((field) => [field.split('^')[0], {}])),
require_field_match: false,
},
}
axios({
Expand All @@ -57,10 +89,18 @@ export default {
headers: { 'Content-Type': 'application/json', Authorization: auth },
data: JSON.stringify(esQuery),
}).then((response) => {
this.results[name] = response.data?.hits ?? []
this.results.count += this.results[name]?.hits?.length ?? 0
self.results[name] = response.data?.hits ?? []
self.resultsCount += self.results[name]?.hits?.length ?? 0
})
})
}, self.debounce)
},
methods: {
highlight(hit, field) {
let source = hit._source ?? hit.source
let highlight = hit.highlight ?? hit.source.highlight
return highlight?.[field]?.[0] ?? source?.[field] ?? ''
},
},
}
Expand Down
49 changes: 17 additions & 32 deletions resources/views/layouts/partials/header/autocomplete.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@
placeholder="@lang('Search')"
class="{{ $inputClasses }}"
v-on:focus="$root.loadAutocomplete = true"
v-if="!$root.loadAutocomplete">
v-if="!$root.loadAutocomplete"
>

<autocomplete
v-if="$root.loadAutocomplete"
v-on:mounted="() => window.document.getElementById('autocomplete-input').focus()"
v-cloak
:additionals="{ categories: ['name^3', 'meta_description^1'] }"
v-bind:additionals="{{ json_encode(config('rapidez.frontend.autocomplete.additionals')) }}"
v-bind:debounce="{{ config('rapidez.frontend.autocomplete.debounce') }}"
v-bind:size="{{ config('rapidez.frontend.autocomplete.size') }}"
class="w-full"
v-cloak
>
<x-rapidez::reactive-base slot-scope="{ results, searchAdditionals, additionals }">
<x-rapidez::reactive-base slot-scope="{ results, resultsCount, searchAdditionals, debounce, limit, highlight }">
<data-search
placeholder="@lang('Search')"
v-on:value-selected="search"
Expand All @@ -28,8 +31,8 @@ class="relative [&_*]:!m-0 [&_[isclearicon=]]:!mr-2 [&_.cancel-icon]:!fill-[#595
:field-weights="Object.values(config.searchable)"
:show-icon="false"
fuzziness="AUTO"
:debounce="100"
:size="10"
:debounce="debounce"
:size="size"
v-on:value-change="searchAdditionals($event)"
>
<div
Expand All @@ -38,33 +41,15 @@ class="relative [&_*]:!m-0 [&_[isclearicon=]]:!mr-2 [&_.cancel-icon]:!fill-[#595
>
<div
class="{{ config('rapidez.frontend.z-indexes.header-dropdowns') }} absolute -inset-x-10 top-full max-h-[600px] overflow-auto rounded-b-xl border bg-white p-2 md:p-5 shadow-xl md:inset-x-0 md:w-full md:-translate-y-px"
v-if="isOpen && (suggestions.length || results.count)"
v-if="isOpen && (suggestions.length || resultsCount)"
>
<div class="flex gap-5 pb-5 my-2" v-if="results.categories && results.categories.hits.length">
<div class="font-bold">
@lang('Categories')
</div>
<ul class="flex flex-col gap-1">
<li v-for="hit in results.categories.hits" class="w-full">
<a :href="hit._source.url" class="w-full hover:text-primary flex gap-1">
<span class="ml-2">@{{ hit._source.name }}</span>
</a>
</li>
</ul>
</div>
<ul class="gap-5 grid md:grid-cols-2">
<li
v-for="suggestion in suggestions"
:key="suggestion.source._id">
<a :href="suggestion.source.url | url" class="flex flex-wrap flex-1" key="suggestion.source._id">
<img :src="'/storage/{{ config('rapidez.store') }}/resizes/80x80/magento/catalog/product' + suggestion.source.thumbnail + '.webp'" class="self-center object-contain w-14 aspect-square" />
<div class="flex flex-1 flex-wrap px-2">
<strong class="hyphens block w-full">@{{ suggestion.source.name }}</strong>
<div class="self-end">@{{ suggestion.source.price | price }}</div>
</div>
</a>
</li>
</ul>
<template v-for="(resultsData, resultsType) in results ?? {}" v-if="resultsData?.hits?.length">
@foreach (config('rapidez.frontend.autocomplete.additionals') as $key => $fields)
@includeIf('rapidez::layouts.partials.header.autocomplete.' . $key)
@endforeach
</template>

@include('rapidez::layouts.partials.header.autocomplete.products')
</div>
</div>
</data-search>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<div class="flex gap-5 pb-5 my-2" v-if="resultsType == 'categories'">
<div class="font-bold">
@lang('Categories')
</div>
<ul class="flex flex-col gap-1">
<li v-for="hit in resultsData.hits" class="w-full">
<a :href="hit._source.url" class="w-full hover:text-primary flex gap-1">
<span class="ml-2" v-html="highlight(hit, 'name')"></span>
</a>
</li>
</ul>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<ul class="gap-5 grid md:grid-cols-2">
<li
v-for="suggestion in suggestions"
:key="suggestion.source._id">
<a :href="suggestion.source.url | url" class="flex flex-wrap flex-1" key="suggestion.source._id">
<img :src="'/storage/{{ config('rapidez.store') }}/resizes/80x80/magento/catalog/product' + suggestion.source.thumbnail + '.webp'" class="self-center object-contain w-14 aspect-square" />
<div class="flex flex-1 flex-wrap px-2">
<strong class="hyphens block w-full" v-html="highlight(suggestion, 'name')"></strong>
<div class="self-end">@{{ suggestion.source.price | price }}</div>
</div>
</a>
</li>
</ul>

0 comments on commit a66c245

Please sign in to comment.