NAV Navbar

Search.js

Search.js is a self-hosted JavaScript library which can be used to rapidly build an interactive, single-page-application user interface around the Luigi’s Box Search API.

You can integrate Luigi's Box search by including the search.js script, setting configuration parameters and providing custom templates to customize the visual appearance.

Features

There is more to search than just search results. Besides support for typical search-related components, such as pagination and sorting, search.js also supports more advanced features.

Faceted search

With search.js, you can rapidly build search user interface which supports advanced filtering and drill-downs. Your users can enter a text query and quickly drill down to search results, by e.g., specifying a category (or categories) they are interested in and selecting a price range.

Search.js will automatically build facets from your data, you only need to set a configuration option.

"Search also in..."

You usually have one primary type of content (products/articles/...) and search.js takes care of searching within this content and generating facets for it. However, you might also want to provide results matching some other types of content (categories/brands/...), regardless of generated facets and selected filters.

For example, you can setup search.js to search primarily in products but also look into categories and brands. When your users search for "samsung tv" they will see a list of products matching this query along with facets allowing them to filter by by various criteria such as screen size or resultion. Plus, they will also see another list with matched brands (e.g., "Samsung") and categories (e.g., "TVs"). These results will not be affected by faceting.

"You might also like"

Great search lets you make mistakes. You can configure search.js to separate results with good match from results with an approximate match. You can keep your list of results clean while at the same time provide additional results which might be of interest to your visitors. These can also effectively circumvent getting a "no results" in case when there is no good match between results and user query.

Redirecting to category listings

You can setup search.js to skip the search altogether when the user's query matches a category and redirect the user to this category listing instead. When you spent time optimizing category listings, you probably prefer to send your users to these instead of fulltext search results.

Note that this feature requires that you are indexing a type with a name category.

Custom results rendering

When you want to render search results using search.js, you have access to all the data that you have indexed for that content — all the data is accessible from within the result template. However, sometimes your rendering requires access to data that you have not indexed or that is even impossible to index properly, e.g. number of visitors currently looking at the product.

You can setup search.js to use your custom server-side rendering API to render the individual results. Search.js will call the API with the product IDs and use the HTML fragments generated by the API as the results representation.

Integration

By following this guide you will configure your site, such that when your users type into the search box and press Enter, their browser will send them to a search page where our search.js library requests search results and renders the search UI.

Example layout for the /search page

<html>
  <body>
    <nav></nav>
    <input type="search" name="q" id="q"/>
    <div id="search-ui">
      <!-- empty placeholder for search UI -->
    </div>
    <footer></footer>
  </body>
</html>

1. Create new search page

Create a new HTML page (e.g., /search) which includes your standard application layout (i.e., header, menus, footer, etc.) and define an empty placeholder element where search.js will render search UI.

2. Submit all search queries to new search page

Update your existing search forms to submit to this newly created webpage via GET method (default when no method is given). You need to ensure that when users type in a query and press Enter or click the search button, they are sent to page you created in step 1 and the query is passed as an URL parameter.

The easiest way to do this, is to wrap all your search fields with a <form> tag, with action attribute set to the path of the page from step 1. E.g. <form action="/search"><input name="q" type="search"></form>

If your search fields are already wrapped in a <form> tag, then just update the action attribute and make sure that the form is submitted via GET method.

<script type="text/x-template" id="..">
</script>
<script type="text/x-template" id="..">
</script>

<!-- Make sure that you define your templates before you load the search.js script -->
<script src="https://cdn.luigisbox.com/search.js"></script>
<script>
  Luigis.Search({
    TrackerId: '2291-22343',
    Size: 20,
    Facets: ['brands', 'categories', 'price_amount'],
    DefaultFilters: {
       type: 'item'
    },
    UrlParamName: {
      QUERY: 'q',
    }
  }, '[name="q"]', '#search-ui')
</script>

3. Setup search.js

Include the script and set configuration parameters. See the right column for an example.

Please note that:

  1. You must define your templates before you load search.js script. Templates are looked up when search.js loads and when they are not present in the page at that time, search.js will fall back to the default built-in templates.
  2. You must initialize the search by calling Luigis.Search. The initialization function takes 3 arguments: configuration object, CSS selector for the input element and CSS selector for the placeholder element where it will render the Search UI.
  3. You must define the initialization script (call to Luigis.Search) in the HTML after the search input element and after the placeholder element. The script expects to find both elements on initialization.
  4. It is important that the search form input name parameter matches the query parameter (UrlParamName.QUERY) in configuration.

Without defining custom templates, you will get a very simple and unstyled search UI. You will most likely want to define custom templates where you can reuse your existing styles.

If you define the templates to match the HTML you are using today, there should be no extensive styling necessary.

Content Security Policy

If your website is using Content Security Policy, you need to add following rules to allow Luigi's Box search.js to work.

CSP directive Value
connect-src https://live.luigisbox.com
script-src https://cdn.luigisbox.com

Options reference

Luigis.Search accepts these arguments:

Option Comment  
TrackerIdREQUIRED Identifier of your site within Luigi's Box
Sizeoptional Specifies how many results should the API return in each request (default: 10)
ResultsRendereroptional When the ResultsRenderer parameter is present, search.js invoke this URL via GET, replacing ${result_ids} placeholder with the comma-separated IDs of all results that should be rendered. For example https://example.shop/api/teasers?ids=23,456,33,62. Returned result will be rendered instead of render template.
Facetsoptional Array of default facets requested in every search request
DefaultFiltersoptional Object with key/value pairs. Value can be string or array of strings. You can use this to setup a default search filters, regardless of user's selection in the search UI.
DefaultSortoptional Sort specification in format field:direction, where field is the field name that you want to sort on, and direction can be either asc or desc.
QuicksearchTypesoptional Array or string of types to run quicksearch on. This is useful for showing hits in categories or brands.
UrlParamNameoptional Sets the URL parameter names. This is useful if you want to preserve your existing URLs, because you can map your existing URL parameters to search.js parameters. See the default values in the right column.
OnDoneoptional A function called after results are rendered (even when ResultsRenderer is used).
SeparateExactResultsoptional Boolean indicating whether to separate exact results from non exact (default: false). When set to true, you need to add <additional-results> component to template to show non exact results.
FollowSuggestedUrloptional Boolean indicating whether to redirect users to a matching category (default: false).

Luigis.Search also accepts two mandatory CSS selectors:

Search input: - [name="q"]: Selector for search input. This will trigger search when enter key is pressed in the selected input field, OR if the parent form is submitted.

Search element: - #search: Id selector for element where Luigi’s Box search component should be rendered.

Templates

Luigi's Box Search.js is using templates to render the Search UI. While we include all templates in the defualt search.js distribution, they are not styled. Usually, you will want to define your custom template which match the styling of your site. Templates are using Vue.js template syntax under the hood.

You should define these templates directly in your HTML code. Each template must be defined in its own <script type="text/x-template> tag. Templates are looked up by their id atribute — make sure to not change it. You don't have to redefine every template, only those that you will actually use.

In each template you have access to these variables which you can use to adjust the layout:

Search template

Example of main search template

<script type="text/x-template" id="template-search">
    <div class="row">
        <div class="col-4">
            <facets></facets>
        </div>
        <div class="col-8">
            <h1>Search results for query "{{ query }}", {{ hitsCount }} results</h1>
            <div class="pull-right">
                <sort></sort>
            </div>
            <loading></loading>
            <quick-search type="brand"></quick-search>
            <quick-search type="category"></quick-search>
            <results></results>
            <pagination></pagination>
        </div>
    </div>
</script>

This is the root template used for rendering search layout. Use this template to define how your Search UI should look and which features it should support. Should it support faceting and sorting? Should the pagination component go above or below search results?

You can reference these main components:

Facets component

Default facets component definition

<script type="text/x-template" id="template-facets">
    <div>
        <facet :facet="facet" v-for="(facet, i) in facets" :key="i"></facet>
    </div>
</script>

Referenced as <facets>.

Used for generating list of facets. The default definition will render each facet in a separate div. Override this template if you want to render facets in a custom structure, such as <ul> list.

Name Description
facets Array of facets

To render a single facet reference <facet> component. <facet> component is a special component that will detect facet type and render a template designed specifically for that component. The <facet> component accepts these arguments:

Name Description
:facet A facet object
:key Recommended unique value. Used for better performance while rendering. See Vue key

Note that you have to explicitly ask for the facets to be generated by setting the Facets initialization option.

Multichoice facet

Default multichoice facet template

<script type="text/x-template" id="template-facet-multichoice">
  <div class="lb-facet-multichoice">
    <h1>{{ name }}</h1>
    <ul class="lb-facet__list">
      <li v-for="val in values">
        <label>
          <input v-model="val.used" type="checkbox"/>
          {{ val.value }} ({{ val.hits_count }})
        </label>
      </li>
    </ul>
  </div>
</script>

Multichoice facet is the most common facet type. It represents a filter that can take multiple choices at the same time. A "brand" facet is a good example: users can search for "phone" and then tick "Apple" and "Samsung" in the brand facet to search only for Apple or Samsung phones.

Name Description
name Facet name. Note that this is the field name (in your source data) and not a human readable label so you will need to translate it for display
values Array of objects with histogram data. Each object contains:
values.hits_count Number of results that will remain when users selects this choice. This value is computed from the current query and values of all other active filters
values.value Choice value
values.used Boolean flag indicating whether this choice is selected

Tips

<h1 v-if="name === 'brand'">Brand</h1>
<h1 v-if="name === 'category'">Category</h1>

To map the facet name to a human readable label, use the v-if directive. See the example on the right.

Range facet

<script type="text/x-template" id="template-facet-numeric-range">
  <div class="lb-facet-range">
    <h1>{{ name }}</h1>
    From {{ min }} - To {{ max }} ({{ value }})
    <vue-slider :min="min" :max="max" v-model="value" @callback="callback"></vue-slider>
  </div>
</script>

Range facet allows you to build a slider-like filtering component. Range facets are most commonly used for prices, where users can select their acceptable price range by adjusting a slider.

Range facet is automatically built from your numerical attributes. When your source data includes integer or floating point attribute and you request a facet for it, we will automatically build a range facet.

Search.js bundles a <vue-slider> component which you can use to easilly build a slider.

Template parameters

Name Description
name Facet name. Note that this is the field name (in your source data) and not a human readable label so you will need to translate it for display
min The minimal value of this attribute with respect to other facets and query. You may have a product priced at $1, but when the user sets e.g. a brand filter to "Samsung", then the minimum value will be the minimum price of Samsung products.
max The maximum value if this attribute with respect to other facets and query.
value Currently selected range encoded as the 2-elements array. The [0] element is the range lower bound, [1] is the range upper bound.
callback Function that you need to call after the user changes the range to trigger search. If you are using the bundled <vue-slider>, then pass the callback as @callback (see the example on the right.

Date facet

Default date facet template

<script type="text/x-template" id="template-facet-date">
  <div class="lb-facet-date">
    <h1>{{ name }}</h1>
    {{ dates }}
    <div>
        Smaller than<br>
        <button @click="onDateChange({dates, options: {smallerThan: true}})">get</button>
    </div>
    Exact day:
    <lb-datepicker id="datepicker" :dates="dates" @change="onDateChange"></lb-datepicker>
    Bigger then:
    <lb-datepicker id="datepicker" :dates="dates" @change="onDateChange" :options="{biggerThan: true}"></lb-datepicker>
    Range:
    <lb-datepicker id="datepicker-range" :dates="dates" :options="{mode: 'range'}" @change="onDateChange"></lb-datepicker>
  </div>
</script>

Date facet allows you to build a calendar-based filter. Date facets are automatically built from your source data attributes which contain date values.

Search.js bundles a custom <lb-datepicker> component which you can use to build a user-friendly calendar selection filters.

Template parameters

Name Description
name Facet name. Note that this is the field name (in your source data) and not a human readable label so you will need to translate it for display
dates Currently selected date range encoded as the 2-elements array. The [0] element is the range lower bound, [1] is the range upper bound.
onDateChange Function that you need to call after the user changes the date range to trigger search. If you are using the bundled <lb-datepicker> component, then pass the callback as @change (see the example on the right.

Luigi's Box datepicker component

Component invocation

<lb-datepicker id="unique-id" :options="optionsObject" @change="onDateChange"></lb-datepicker>

The <lb-datepicker> supports 2 different modes, integrated with the Luigi's Box Search API so you can just set the mode and search.js will send the correct search API request:

The range mode can be furthermore specialized to: - Later than mode, where a user can pick a single date and search.js will make a search request and specify that the attribute value must be bigger than (later than) the selected date. - Sooner than mode, where a user can pick a single date and search.js will make a search request and specify that the attribute value must be sooner than (smaller than) the selected date.

optionsObject example

{
    dateFormat: 'Y-m-d', // Displayed date format
    mode: 'range'        // If you want to select multiple dates
    biggerThan: true,    // Optional, set to true to turn on the *Later than* mode
    smallerThan: true,   // Optional, set to true to turn on the *Sooner than* mode
}
Name Description
idREQUIRED Each datepicker must have a unique ID.
options JavaScript Object with options. See full reference on the right.

Using custom datepicker component

Example onDateChange to execute range search

onDateChange({
  dates: [new Date('2018-01-20'), new Date('2018-12-31')]
})

Example onDateChange to execute later than search

onDateChange({
  dates: [new Date('2018-01-20')],
  options: {
    biggerThan: true
  }
})

Example onDateChange to execute sooner than search

onDateChange({
  dates: [new Date('2018-01-20')],
  options: {
    smallerThan: true
  }
})

If you want to use your custom date picker component, make sure to call the onDateChange callback to trigger search. The function takes a single Object argument with 2 keys: dates and options

Name Description
dates Array. In range mode, use 2 values to denote interval. In later than or sooner than mode use an Array with single date.
options None for single and standard range search. {biggerThan: true} for later than search, {smallerThan: true} for sooner than search.

Boolean facet

Default boolean facet template

<script type="text/x-template" id="template-facet-boolean">
  <div class="lb-facet-bool">
    <label>
      {{ name }}
      ({{ hits_count }})
      <input v-model="value" type="checkbox"/>
    </label>
  </div>
</script>

Boolean facet is useful for filtering on boolean attributes. Boolean facet is automatically generated for all boolean fields in your source data.

It is often used for "In stock" or "Free shipping" filtering.

Note, that the semantics of the boolean facet is that it can be used to filter for "true"-ness of an attribute, or no filtering at all. If you have an "In stock" facet, rendered as checkbox:

Name Description
name Facet name. Note that this is the field name (in your source data) and not a human readable label so you will need to translate it for display
hits_count Number of results that will remain when users selects this choice. This value is computed from the current query and values of all other active filters

Sort component

Default sort component template

<script type="text/x-template" id="template-sort">
    <div>
        <a href="#" v-if="sortBy === 'price'"
           class="lb-sort lb-sort--active"
           :class="'lb-sort--' + sortDir"
           @click.prevent="doSort('price:' + sortDirReverse)">
            Price {{ sortDir }}
        </a>
        <a href="#" v-if="sortBy !== 'price'"
           class="lb-sort lb-sort--asc"
           @click.prevent="doSort('price:asc')">
            Price
        </a>
    </div>
</script>

The sort component allows you to build a UI element where users can change the way the search results are sorted.

When building a sort component, you must decide the attributes which you want to sort on. Our recommendation is to keep things simple and keep "less is more" on your mind.

When you don't specify any sort parameters, Luigi's Box Search API will order the results by our proprietary sorting algorithm, which in many cases is all you need. Often times, sorting is used as a workaround in the absence of more powerful faceting functions. Take for example "Sort by availability". You can provide vastly enhanced user experience and convenience when you implement this as an "availability" facet. This way, your users can instantly see how many items are in stock, how many are ready for immediate shipment, how many are unavailable, etc., and they are able to drill down to specific availability states they are interested in, while also keeping the filtered items sorted by relevance.

Template parameters

Name Description
sortBy String. Name of the attribute the results are currently sorted by. This can be null if no explicit sort was requested.
sortDir String. Sort direction (asc
sortDirReverse String. Reverse sort direction (desc
doSort Function. Call this function to execute search with the requested sorting parameters.

Loading component

Default loading component template

<script type="text/x-template" id="template-loading">
    <div v-if="isLoading">
        LOADING ... {{ isLoading }}
    </div>
</script>

Loading component is visible only during the time we are requesting resources from external APIs. This includes calls to Luigi's Box Search API to retrieve the results and (if you are using it), calls to your own ResultsRenderer API.

This component is mainly useful if you want to provide a global loading indicator, e.g. an overlay over search results with a spinner animation.

You can also access special variables in any of the components:

Having the isLoading flag available in every component allows you to build very flexible loading states. Want to fade out the Search UI on loading? Use a <v-if="isLoading"> on the main Search template to set a specific CSS class. Want to replace facet templates with a loading animation for each facet? Use a <v-if="isLoading"> in the Facet template to render a different HTML when loading.

Results component

Default results component template

<script type="text/x-template" id="template-results">
    <div>
        <result :result="result" v-for="(result, i) in results" :key="i"></result>
    </div>
</script>

Referenced as <results>.

Used for rendering search results.

Template rendered when no results were found

<script type="text/x-template" id="template-no-results">
    <div class="lb-no-results">
        No results found
    </div>
</script>

Note that in case the Search API returns no results, search.js will render template with id template-no-results.

Template parameters

Name Description
hitsCount Number of search results in total (including those that are not displayed and only accessible via pagination) that are matching the queries and filters.
results Array of search results that you should individually pass to a <result> component via its :result property.

Single result component

<script type="text/x-template" id="template-result-default">
    <div class="lb-result-default">
        {{ attributes.title }} - {{ url }}
    </div>
</script>

Referenced as <result>

Use this component to render a single product representation. You can directly reference all attributes that you have indexed for this type of content.

For convenience, each result can have the template based on its type.

If the result is of type product, template engine will search for template id template-result-product, and if not found fallback to template with id template-result-default.

<script type="text/x-template" id="template-result-{type}">
    <div>
        Result {{ type }}
    </div>
</script>

Additional results component

Referenced as <additional-results>.

Used for displaying additional results only when SeparateExactResults option set to true. When set up, then

When there are no approximate matches, then this component will not be rendered. When all matches are approximate, then by default all of them will be rendered inside <results> component

<additional-results>
    <h2>You may also like</h2>
    <div slot="after">Show after additional results</div>
</additional-results>

Content wrapped inside <additional-results> will be displayed before additional results. You can use this to show a title. You can use a special "after" slot marker to display content after additional results, e.g., <div slot="after">.

Since this content is wrappped inside the component template it is not shown when the component is not shown, e.g., when there are no approximate matches.

This component is using the same mechanism and templates as the <results> component.

If you are using ResultsRenderer, then the product representation will be generated by your external ResultsRenderer API via a separate API call. If part of your search result hits are exact and part is approximate, search.js will call the ResultsRenderer endpoint twice. You should consider this additional load in your capacity planning.

Pagination component

Referenced as <pagination>.

Used for displaying pagination component. Search.js only supports the "next-page" style pagination where users can request another page of results and the next page of results is appended to the already displayed results.

<script type="text/x-template" id="template-pagination">
    <div class="lb-pagination">
        <a href class="lb-pagination__pager lb-pagination__next"
           v-if="isNextPage"
           @click.prevent="nextPage"
        >
            Load page {{ page }}
        </a>
    </div>
</script>
Name Description
page Integer. Next page number.
isNextPage Boolean. Indicates whether a next page of results is available.
nextPage Function. Call this function to trigger loading of next page of results.

"Also search in" component

Referenced as <quick-search>

<script type="text/x-template" id="template-quick-search-{type}">
    <div v-if="items.length > 0">
        Quick search name
        <div v-for="item in items">
            {{ item.attributes.title }} - {{ item.url }}
        </div>
    </div>
</script>

Displays matches within additional types if QuicksearchTypes option is present. These templates are scoped to specified type. Each type must have its own template with the type name embedded in the template id. E.g., if quicksearch type is brand, template engine will search for template-quick-search-brand.

Name Description
items Array. Array of quick search results for current type. You can access the indexed attributes directly on each array element.