Implementing a search function

There are 2 ways to implement a search function in Kuroco:

  • using a filter
  • using search providers such as Algolia and Syncsearch

This tutorial explains how to implement a content search function using the first approach.

Implementing a search function using a filter

An API filter function allows you to implement a content search with conditions or keywords.

fetched from Gyazo

First, let us review what a filter function does.

A filter function is used to filter the data retrieved from an API endpoint. For example, entering the query below as the "filter" parameter returns data that matches the condition specified:

// specify contents with a topics_id of 1
topics_id = 1

You can specify flexible conditions such as complete or partial matches, number and date comparisons, and AND/OR. This enables you to implement complex searches.

// Specify contents whose title includes "WORD" or that were added between 2021-01-01 and 2021-12-31
subject contains "WORD" OR (inst_ymdhi >= "2021-01-01" AND inst_ymdhi <= "2021-12-31")

You can specify the filter parameter in the locations below according to what function it serves.

LocationDetail
Endpoint settingSets fixed search conditions that are always applied.
For example, entering inst_ymdhi >=: relatively "-1 year" here retrieves only contents added within a year from the current date and time.
GET parameterSpecifies dynamic search conditions.
If a filter has been set in the endpoint settings, the search results will match both conditions below:
filter query(endpoint setting) AND filter query(GET parameter)

To change the retrieved data based on user input, specify the filter query with the GET parameter.
To add a fixed condition, enter the query in the endpoint settings as needed.

Note: A filter can only be used when the API model supports the function. Look for the "filter" parameter on the endpoint settings screen or Swagger UI screen.

The next section explains how to implement a search function using a filter. This tutorial covers 2 types of searches:

  • conditional search
  • keyword search

Below, we explain how to implement a conditional search based on the function overview covered above.

1. Create a content structure and an endpoint

Follow the steps below to create a content structure and an endpoint.

Content structure
In the left sidebar menu, click [Content structure] -> [Add].

Image (fetched from Gyazo)

Create a content structure with the following settings:

  • Group name: Search
  • Group ID: 34 (automatically assigned)

fetched from Gyazo

Add the following items using the extra columns.

IDItem nameItem settingsOptions
ext_col_01TextText-
ext_col_02SelectSelectBox format01::option1
02::option2
03::option3
ext_col_03CheckboxMultiple choice (Checkbox)01::option1
02::option2
03::option3

fetched from Gyazo

Add some test data to the "Search" group you have just created.

fetched from Gyazo

Endpoint creation

In the left sidebar menu, click [API] and select the desired API to access its endpoint list screen. Click [Configure Endpoint].
fetched from Gyazo

Create an endpoint with the following settings:

ItemValue
Path(/rcms-api/**/) content
CategoryContent
ModelTopics (v1)
Operationlist
topics_group_idID of "Search" group

fetched from Gyazo

fetched from Gyazo

Note: The ID of the group can be found on the content structure list screen.
fetched from Gyazo

2. Configure the endpoint

On the search endpoint list screen, click the [Update] button for the endpoint you created.

fetched from Gyazo

On the pop-up screen, scroll down to the filter_request_allow_list field.

Image (fetched from Gyazo)

By default, the filtering of queries by the GET parameter is disabled. Specifying the search items allowed using filter_request_allow_list activates the filter.

For this tutorial, add the following items:

  • subject
  • inst_ymdhi
  • ext_col_01
  • ext_col_02
  • ext_col_03

fetched from Gyazo

Note:
Entering :ALL here allows all items to be searched. While this is convenient, it sometimes weakens API security. (For example, items that have not been set as valid response data will be also allowed as search items.)
Therefore, we generally recommend setting each search item individually.
If you use :ALL, check to ensure that there are no issues with the data returned by the endpoint.

After you have specified the desired items, click [Update] to save the changes.

fetched from Gyazo

3. Verify the endpoint operations

On the API endpoint list screen, click [Swagger UI].

fetched from Gyazo

Under "Content" on the API Swagger UI screen, click the endpoint you have configured. This opens a dropdown panel.

fetched from Gyazo

Click [Try it out] to begin the verification.

fetched from Gyazo

Enter a query in the "filter" parameter. For this tutorial, enter subject contains "Test".

fetched from Gyazo

After you have entered your query, scroll down to the bottom and click [Execute].

fetched from Gyazo

Verify the responses displayed.

fetched from Gyazo

4. Implement the search function

Next, implement a search function using the configured endpoint.

For this tutorial, we use Nuxt.js to create components for a simple search form and search results table.

fetched from Gyazo

First, create the following component.

<template>
  <div>
    <div class="search-form">
      <p>
        <label for="subject">Title</label>
        <input v-model="searchInput.subject" type="text">
      </p>
      <p>
        <label for="inst_ymdhi">Created at</label>
        <input v-model="searchInput.inst_ymdhi.from" type="date"> 
        ~
        <input v-model="searchInput.inst_ymdhi.to" type="date"> 
      </p>
      <p>
        <label for="ext_col_01">Text</label>
        <input v-model="searchInput.ext_col_01" type="text">
      </p>
      <p>
        <label for="ext_col_02">Select</label>
        <select v-model="searchInput.ext_col_02">
          <option value="">Not selected</option>
          <option value="01">option1</option>
          <option value="02">option2</option>
          <option value="03">option3</option>
        </select>
      </p>
      <p>
        <label for="ext_col_03">Checkbox</label>
        <input v-model="searchInput.ext_col_03" type="checkbox" value="01">option1
        <input v-model="searchInput.ext_col_03" type="checkbox" value="02">option2
        <input v-model="searchInput.ext_col_03" type="checkbox" value="03">option3
      </p>  
      <button type="button">Search</button>
    </div>
    <div v-if="Object.keys(searchResult).length > 0" class="search-result">
      <template v-if="(searchResult.errors || []).length === 0">
        <table>
          <tr>
            <th>ID</th>
            <th>Title</th>
            <th>Created at</th>
            <th>Text</th>
            <th>Select</th>
            <th>Checkbox</th>
          </tr>
          <tr v-for="content in searchResult.list" :key="content.topics_id">
            <td>{{ content.topics_id }}</td>
            <td>{{ content.subject }}</td>
            <td>{{ content.inst_ymdhi }}</td>
            <td>{{ content.ext_col_01 }}</td>
            <td>{{ content.ext_col_02 }}</td>
            <td>{{ content.ext_col_03 }}</td>
          </tr>
        </table>
      </template>
      <template v-else>
        {{ searchResult.errors }}
      </template>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      searchInput: {
        subject: '',
        inst_ymdhi: {
          from: '',
          to: '',
        },
        ext_col_01: '',
        ext_col_02: '',
        ext_col_03: []
      },
      searchResult: {},
    }
  },
  mounted() {},
  methods: {}
}
</script>

<style scoped>
.search-form {
  border: 1px solid;
  padding: 10px;
}
.search-form label {
  display: block;
  float: left;
  width: 100px;
}
.search-result {
  width: 100%;
  margin-top: 20px;
}
.search-result table, th, td {
  border: solid 1px;
  border-collapse: collapse;
}
.search-result th, td {
  padding: 5px;
}
.search-result table {
  width: 100%;
}
</style>

The .search-form element displays a search form, and .search-result shows a search result.

Any user-entered values are stored in searchInput within the data object. The corresponding search result responses are stored in searchResult.

data() {
  return {
    // User's input (.search-form)
    searchInput: {
      subject: '',
      inst_ymdhi: {
        from: '',
        to: '',
      },
      ext_col_01: '',
      ext_col_02: '',
      ext_col_03: []
    },
    // search result response (.search-result)
    searchResult: {},
  }
},

Run the server and check the site on your browser. It should display the form below.

fetched from Gyazo

At this stage, nothing happens if you click the [Search] button, because you haven't implemented the endpoint call process. To do so, you must define methods and implement a search process by calling an endpoint.

First, create a search method. Write your process to call the endpoint you configured above and name it "2. Endpoint settings".

This method transfers the filter query generation process to the buildFilterQuery method. (The detailed mechanisms will be explained later.)

methods: {
  // Send request to the endpoint and store the fetch result in 'searchResult'
  search() {
    // Replace below endpoint URL with the one you have configured
    this.$axios.get("https://example.g.kuroco.app/rcms-api/1/content", {
      params: {
        filter: this.buildFilterQuery()
      }
    }).then(response => {
      this.searchResult = response.data || {};
    }).catch(({ response }) => {
      this.searchResult = !(response instanceof Object) || !Array.isArray(response.data.errors)
        ? { errors: ['Unexpected error'] }
        : response.data;
    });
  },
  // Generate filter query
  buildFilterQuery() {
    return '';
  }
}

Second, define the search method as a click event of the [Search] button, which is located at the bottom of the .search-form element.

<!--
    <button type="button">Search</button> <= add '@click="search"'
-->
<button type="button" @click="search">Search</button>

Third, write the specific process of buildFilterQuery. It converts the value stored in the searchInput data attribute to a filter query.

For this tutorial, we generate a query with the following conditions:

  • Date: range specification
  • Text: partial match
  • Select: perfect match
  • Multiple select (checkbox): partial match

Items with a blank value are not included in the search conditions.

Note: The generated query varies depending on the input format, item, and functional requirements. Please implement the appropriate process as needed.

methods: {
  // ...
  buildFilterQuery() {
    const filterQuery = Object.entries(this.searchInput).reduce((queries, [col, value]) => {
      switch (col) {
        // date: range specification
        case 'inst_ymdhi':
          if (value.from !== '') {
            queries.push(`${col} >= "${value.from}"`);
          }
          if (value.to !== '') {
            queries.push(`${col} <= "${value.to}"`);
          }
          break;
        // text: partial match
        case 'subject':
        case 'ext_col_01':
          if (value !== '') {
            queries.push(`${col} contains "${value}"`);
          }
          break;
        // select: perfect match
        case 'ext_col_02':
          if (value !== '') {
            queries.push(`${col} = "${value}"`);
          }
          break;
        // multiple select(checkbox): partial match
        case 'ext_col_03':
          if (value.length > 0) {
            queries.push('(' + value.map(v => `${col} contains "${v}"`).join(' OR ') + ')');
          }
          break;
        default:
          break;
      }
      return queries;
    }, []).join(' AND ');

    return filterQuery;
  }
}

Lastly, add the following code to mounted to display the contents list without condition specifications by default.

mounted() {
  this.search();
},

Now you have implemented the conditional search component. Test the form on your localhost to verify the results.

fetched from Gyazo

This section explains how to implement a keyword search function.

Based on the above conditional search implementation, you might have thought of connecting partial match conditions for each item with OR.

subject contains "KEYWORD" OR ext_col_01 contains "KEYWORD"

You can create a keyword search function simply using this approach. However, as the number of items increases, issues can arise when you pass long, complex queries like this one:

subject contains "KEYWORD" OR ext_col_01 contains "KEYWORD" OR ext_col_04 contains "KEYWORD" OR ext_col_05 contains "KEYWORD" OR ext_col_06 contains "KEYWORD" OR ...

Therefore, the filter function contains the following mechanism to implement simpler keyword search queries:

keyword contains "KEYWORD"

The steps below show you how to implement the keyword function.

1. Create a content structure and an endpoint

For this tutorial, use the following content structure and endpoint:

Content structure

Use the conditional search you implemented above.

Endpoint

Create an endpoint with the following settings:

ItemValue
Path/rcms-api/**/content_keyword
CategoryContent
ModelTopics (v1)
Operationlist
topics_group_idUnique ID of the "Search" group

fetched from Gyazo

fetched from Gyazo

Note: The topics_group_id can be found on the content structure page.
fetched from Gyazo

2. Configure the endpoint

On the search endpoint list page, select the endpoint you want to implement the search function for and click [Update].

fetched from Gyazo

On the pop-up screen, scroll down to the filter_request_allow_list field.

fetched from Gyazo

The allowed search items follow the formats below:

FormatDescription
keywordSearches for all items (e.g., keyword).
keyword:[item 1,item 2,...]Enter multiple search items separated with a comma (e.g., keyword:[subject]; keyword:[ext_col_01,ext_col_02]).

Note: The value :ALL includes keyword as a search item along with the other items specified.

For this tutorial, enter keyword:[subject,ext_col_01] to set the title and text as the search items.

fetched from Gyazo

Click [Update] to save the settings.

fetched from Gyazo

3. Verify the endpoint operations

On the API endpoint list screen, click [Swagger UI].

fetched from Gyazo

On the Swagger UI screen, click the endpoint you created.

fetched from Gyazo

Click [Try it out] to begin the verification.

fetched from Gyazo

For this tutorial, we want to generate a search query with a partial match using the contains command.
Enter keyword contains "1" in the "filter" parameter.

fetched from Gyazo

Click [Execute].

fetched from Gyazo

Verify the responses displayed.

fetched from Gyazo

4. Implement the search function

Next, implement the search function using the configured endpoint.

For this tutorial, we use Nuxt.js to create the components for a simple search form and search result table.

fetched from Gyazo

First, create the following component.

<template>
  <div>
    <div class="search-form">
      <p>
        <label for="keyword">Keyword</label>
        <input v-model="searchInput.keyword" type="text">
      </p>
      <button type="button" @click="search">Search</button>
    </div>
    <div v-if="Object.keys(searchResult).length > 0" class="search-result">
      <template v-if="(searchResult.errors || []).length === 0">
        <table>
          <tr>
            <th>ID</th>
            <th>Title</th>
            <th>Created at</th>
            <th>Text</th>
            <th>Select</th>
            <th>Checkbox</th>
          </tr>
          <tr v-for="content in searchResult.list" :key="content.topics_id">
            <td>{{ content.topics_id }}</td>
            <td>{{ content.subject }}</td>
            <td>{{ content.inst_ymdhi }}</td>
            <td>{{ content.ext_col_01 }}</td>
            <td>{{ content.ext_col_02 }}</td>
            <td>{{ content.ext_col_03 }}</td>
          </tr>
        </table>
      </template>
      <template v-else>
        {{ searchResult.errors }}
      </template>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      searchInput: {
        keyword: '',
      },
      searchResult: {},
    }
  },
  mounted() {
    this.search();
  },
  methods: {
    search() {
      // Replace with the URL of the endpoint you have created.
      this.$axios.get("https://example.g.kuroco.app/rcms-api/1/content_keyword", {
        params: {
          filter: this.buildFilterQuery()
        }
      }).then(response => {
        this.searchResult = response.data || {};
      }).catch(({ response }) => {
        this.searchResult = !(response instanceof Object) || !Array.isArray(response.data.errors)
          ? { errors: ['Unexpected error'] }
          : response.data;
      });
    },
    buildFilterQuery() {
      return '';
    }
  }
}
</script>

<style scoped>
.search-form {
  border: 1px solid;
  padding: 10px;
}
.search-form label {
  display: block;
  float: left;
  width: 100px;
}
.search-result {
  width: 100%;
  margin-top: 20px;
}
.search-result table, th, td {
  border: solid 1px;
  border-collapse: collapse;
}
.search-result th, td {
  padding: 5px;
}
.search-result table {
  width: 100%;
}
</style>

Use the same attribute definitions as the conditional search above, except for the following:

  • the .search-form element in the template
  • the searchInputproperty of the data object
  • the buildFilterQuery method

For the .search-form element, define only the keyword input field. Store the entered value in searchInput.keyword.

<div class="search-form">
    <p>
    <label for="keyword">Keyword</label>
    <input v-model="searchInput.keyword" type="text">
    </p>
    <button type="button" @click="search">Search</button>
</div>
data() {
  return {
    searchInput: {
      keyword: '',
    },
    searchResult: {},
  }
},

As the final step of implementing the keyword search function, edit buildFilterQuery to include the keyword search query generation process.

methods: {
  // ...
  buildFilterQuery() {
    const filterQuery = Object.entries(this.searchInput).reduce((queries, [col, value]) => {
      switch (col) {
        case 'keyword':
          if (value !== '') {
            queries.push(`${col} contains "${value}"`);
          }
          break;
        default:
          break;
      }
      return queries;
    }, []).join(' AND ');
    return filterQuery;
  }
}

Now you are ready to test the form on your localhost and verify the results.

fetched from Gyazo

Appendix: Implementing a search function via a commercial service

You can implement a comprehensive full-text search such as site-wide search using a commercial search service.

While the details of such searches are beyond the scope of this tutorial, we recommend the following services:

If you have any other questions, please use our contact form or Slack workspace.