Implementing a search function

There are two 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's review what a filter function does.

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

// 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 input the filter parameter in the locations below based on what function it serves.

LocationDescription
Endpoint settingsSet 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 parameterSpecify 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 obtain variable results based on user input, set the filter query with the GET parameter.
To add a fixed condition, enter the query in the endpoint settings as needed.

A filter can only be used when the API model supports the function. Look for the filter parameter on the endpoint settings 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

Before you start

Create an API

API

Set up a new API as follows.

FieldValue
TitleFilter function API
Version1.0
DescriptionFor use by filter function

Image from Gyazo

Click [Add] to save these settings. You will be redirected to the Endpoint list screen. Click [Security].

Image from Gyazo

Select "Cookie" and click [Save].

Image from Gyazo

CORS

Next, click [Operation CORS] to configure the CORS settings.

Image from Gyazo

Under "CORS_ALLOW_ORIGINS", add the following entries using [Add Origin].

  • http://localhost:3000
  • (Your front-end domain name)

Under "CORS_ALLOW_METHODS", add the following entries using [Add Method].

  • GET
  • POST
  • OPTIONS

Under "CORS_ALLOW_CREDENTIALS", select "Allow Credentials".

Image from Gyazo

Click [Save] when you are done.

The steps below explain how to implement a conditional search based on the above overview.

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, select [Content structure] -> [Add].

Image (fetched from Gyazo)

Create a content structure with the following settings:

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

Image from Gyazo

Create the following additional fields.

IDField nameField settingsOptions
1TextSingle-line text-
2SelectDropdown selection01::option1
02::option2
03::option3
3CheckboxMultiple choice (checkbox)01::option1
02::option2
03::option3

Image from Gyazo

Add some test data to this content group as follows.

Image from Gyazo

Endpoint creation

In the left sidebar menu, click [API] and select the filter function API to access its endpoint list screen.
Click [Configure Endpoint].

fetched from Gyazo

Create an endpoint with the following settings:

FieldValue
Path(/rcms-api/**/) content
CategoryContent
ModelTopics (v1)
Operationlist
topics_group_id(ID of "Search" content group)

Image from Gyazo

Image from Gyazo

The content group ID can be found on the Content structure list screen.

Image from Gyazo

2. Configure the endpoint

On the search endpoint list screen, click [Update] next to 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, query filtering 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 entries:

  • subject
  • inst_ymdhi
  • ext_1
  • ext_2
  • ext_3

Depending on when you signed up with Kuroco, additional fields may be follow the label format ext_col_01 instead of ext_1. If query filtering does not work using the above settings, please verify the Swagger UI response.

Image from Gyazo

Entering :ALL here allows all fields to be searched. Although this is convenient, it sometimes weakens API security. (For example, fields 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 fields, 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 a simple search form and search results table.

Image from Gyazo

First, create the following component under the file name pages/search/index.vue.

<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_1">Text</label>
        <input v-model="searchInput.ext_1" type="text">
      </p>
      <p>
        <label for="ext_2">Select</label>
        <select v-model="searchInput.ext_2">
          <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_3">Checkbox</label>
        <input v-model="searchInput.ext_3" type="checkbox" value="01">option1
        <input v-model="searchInput.ext_3" type="checkbox" value="02">option2
        <input v-model="searchInput.ext_3" 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_1 }}</td>
            <td>{{ content.ext_2 }}</td>
            <td>{{ content.ext_3 }}</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_1: '',
        ext_2: '',
        ext_3: []
      },
      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 input values (.search-form)
    searchInput: {
      subject: '',
      inst_ymdhi: {
        from: '',
        to: '',
      },
      ext_1: '',
      ext_2: '',
      ext_3: []
    },
    // Search result responses (.search-result)
    searchResult: {},
  }
},

Run the command npm run dev to launch your local environment and go to http://localhost:3000/search. You should see the form below.

Image from Gyazo

At this stage, nothing happens if you click the [Search] button, because you haven't implemented the endpoint call process yet. 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'.
  async search() {
    let searchResult;
    try {
      // Replace the endpoint URL below with your own.
      const response = await this.$axios.get("/rcms-api/3/content", {
        params: {
          filter: this.buildFilterQuery()
        }
      })
      searchResult = response?.data || {};
    } catch(errorResponse) {
      searchResult = { errors: errorResponse?.data?.errors || ['Unexpected error'] };
    }
    this.searchResult = searchResult;
  },
  // Generate filter query
  buildFilterQuery() {
    return '';
  }
}

Substitute /rcms-api/3/content above with the path shown in your own Kuroco admin panel.

Second, define the search method as a click event of the [Search] button, 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: Specify range
  • Text: Partial match
  • Select: Full match
  • Checkbox: Partial match

Fields with no input value are not included in the search conditions.

The generated query varies depending on the input format, target field, and functional requirements. Please implement the appropriate process based on your own environment.

methods: {
  // ...
  buildFilterQuery() {
    const filterQuery = Object.entries(this.searchInput).reduce((queries, [col, value]) => {
      switch (col) {
        // Date: Select range
        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_1':
          if (value !== '') {
            queries.push(`${col} contains "${value}"`);
          }
          break;
        // Select: Full match
        case 'ext_2':
          if (value !== '') {
            queries.push(`${col} = "${value}"`);
          }
          break;
        // Checkbox : Partial match
        case 'ext_3':
          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.

Image 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_1 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_1 contains "KEYWORD" OR ext_4 contains "KEYWORD" OR ext_5 contains "KEYWORD" OR ext_6 contains "KEYWORD" OR ...

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

search_keyword contains "KEYWORD"

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

Some sites work with keyword instead of search_keyword depending on when you applied for Kuroco.
If it does not work well, please try using keyword as well.

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:

FieldValue
Path(/rcms-api/**/)content_keyword
CategoryContent
ModelTopics (v1)
Operationlist
topics_group_id(ID of the "Search" content group)

Image from Gyazo

Image from Gyazo

The content group ID can be found on the Content structure list screen.

Image 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
search_keywordSearches for all items (e.g., search_keyword).
search_keyword:[item 1,item 2,...]Enter multiple search items separated with a comma (e.g., search_keyword:[subject]; search_keyword:[ext_1,ext_2]).

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

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

Image 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].

Image from Gyazo On the Swagger UI screen, click the endpoint you created.

Image from Gyazo

Click [Try it out] to begin the verification.

Image from Gyazo

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

Image 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 endpoint you set up earlier.

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

Image from Gyazo

First, create the following component under the file name pages/search_keyword/index.vue.

<template>
  <div>
    <div class="search-form">
      <p>
        <label for="keyword">Keyword</label>
        <input v-model="searchInput.search_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_1 }}</td>
            <td>{{ content.ext_2 }}</td>
            <td>{{ content.ext_3 }}</td>
          </tr>
        </table>
      </template>
      <template v-else>
        {{ searchResult.errors }}
      </template>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      searchInput: {
        search_keyword: '',
      },
      searchResult: {},
    }
  },
  mounted() {
    this.search();
  },
  methods: {
  async search() {
      let searchResult;
      try {
        // Replace with your own endpoint URL.
        const response = await this.$axios.get("/rcms-api/3/content_keyword", {
          params: {
            filter: this.buildFilterQuery()
          }
        })
        searchResult = response?.data || {};
      } catch(errorResponse) {
        searchResult = { errors: errorResponse?.data?.errors || ['Unexpected error'] };
      }
      this.searchResult = searchResult;
    },
    // Generate filter query.
    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>

Substitute /rcms-api/3/content_keyword above with the path shown in your own Kuroco admin panel.

Run the npm run dev command to launch your local environment and go to http://localhost:3000/search_keyword. You should see the form below.

Image from Gyazo

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.search_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: {
      search_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 'search_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.

Image from Gyazo

Appendix: Using a commercial service

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

The details of these kinds of search functions are beyond the scope of this tutorial. For more information, refer to the following services:

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